From aa3c04a7356ebf3dfcf514739d4bf56f95d022b2 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Thu, 12 Nov 2026 16:15:41 -0500 Subject: [PATCH 1/8] Keep the final-template prune on in grammars with partial rules wherever no partial word can be involved Co-Authored-By: Claude Opus 5.5 --- .../AnalysisAffixTemplateRule.cs | 21 +- .../AnalysisStratumRule.cs | 9 +- .../FinalTemplatePartialReachability.cs | 135 +++++++++ .../Morpher.cs | 56 +++- .../AnalysisAffixProcessRule.cs | 13 +- .../PartialRuleLookAhead.cs | 262 ++++++++++++++++++ src/SIL.Machine.Morphology.HermitCrab/Word.cs | 20 +- 7 files changed, 500 insertions(+), 16 deletions(-) create mode 100644 src/SIL.Machine.Morphology.HermitCrab/FinalTemplatePartialReachability.cs create mode 100644 src/SIL.Machine.Morphology.HermitCrab/PartialRuleLookAhead.cs diff --git a/src/SIL.Machine.Morphology.HermitCrab/AnalysisAffixTemplateRule.cs b/src/SIL.Machine.Morphology.HermitCrab/AnalysisAffixTemplateRule.cs index 407c7fdb1..183c2da57 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/AnalysisAffixTemplateRule.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/AnalysisAffixTemplateRule.cs @@ -41,13 +41,9 @@ public IEnumerable Apply(Word input) if (_morpher.TraceManager.IsTracing) _morpher.TraceManager.BeginUnapplyTemplate(_template, input); - // Do not allow a final template to unapply if we don't need to worry about partial rules - // and a non-template was last unapplied. - if ( - (!_morpher.HasPartialRules || _morpher.AlwaysEnforceFinalTemplates) - && input.FinalTemplateState == FinalTemplateState.NonTemplate - && _template.IsFinal - ) + // In synthesis a non-template rule follows a final template only if a partial rule applied earlier. + bool afterNonTemplate = _template.IsFinal && input.FinalTemplateState == FinalTemplateState.NonTemplate; + if (afterNonTemplate && _morpher.IsFinalTemplatePrunable(_template, input)) { if (_morpher.TraceManager.IsTracing) { @@ -62,6 +58,9 @@ public IEnumerable Apply(Word input) } Word inWord = input.Clone(); + // The prune check above passed, so the word is partial at this template or the analysis fails. + if (afterNonTemplate) + inWord.NeedsPartial = true; inWord.Freeze(); var output = new HashSet(FreezableEqualityComparer.Default); @@ -70,6 +69,14 @@ public IEnumerable Apply(Word input) else ParallelApplySlots(inWord, output); + // The output with every slot skipped did not unapply the template, so it owes nothing. + if (afterNonTemplate && output.Remove(inWord)) + { + Word unchanged = input.Clone(); + unchanged.Freeze(); + output.Add(unchanged); + } + return output; } diff --git a/src/SIL.Machine.Morphology.HermitCrab/AnalysisStratumRule.cs b/src/SIL.Machine.Morphology.HermitCrab/AnalysisStratumRule.cs index ef48637ac..3427cca6b 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/AnalysisStratumRule.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/AnalysisStratumRule.cs @@ -184,6 +184,8 @@ internal IEnumerable Apply(Word input, ref int alternativeCount) private IEnumerable ApplyMorphologicalRules(Word input) { + if (_morpher.OwesImpossiblePartial(input)) + yield break; foreach (Word mruleOutWord in _mrulesRule.Apply(input).Distinct(FreezableEqualityComparer.Default)) { switch (_stratum.MorphologicalRuleOrder) @@ -203,6 +205,8 @@ private IEnumerable ApplyMorphologicalRules(Word input) private IEnumerable ApplyTemplates(Word input) { + if (_morpher.OwesImpossiblePartial(input)) + yield break; foreach (Word tempOutWord in _templatesRule.Apply(input).Distinct(FreezableEqualityComparer.Default)) { switch (_stratum.MorphologicalRuleOrder) @@ -235,6 +239,7 @@ private IEnumerable ApplyTemplates(Word input) private readonly int _nonHeadCount; private readonly IReadOnlyDictionary _ruleCounts; private readonly FinalTemplateState _finalTemplateState; + private readonly bool _needsPartial; private readonly int _hashCode; // Freezes word.SyntacticFeatureStruct, which Word.Freeze leaves unfrozen. @@ -253,6 +258,7 @@ public AnalysisMergeKey(Word word) _nonHeadCount = word.NonHeadCount; _ruleCounts = word.UnappliedRuleCounts; _finalTemplateState = word.FinalTemplateState; + _needsPartial = word.NeedsPartial; _shape.Freeze(); _syntacticFS.Freeze(); @@ -265,6 +271,7 @@ public AnalysisMergeKey(Word word) hash = hash * 31 + _realizationalFS.GetFrozenHashCode(); hash = hash * 31 + _nonHeadCount; hash = hash * 31 + _finalTemplateState.GetHashCode(); + hash = hash * 31 + (_needsPartial ? 1 : 0); if (_ruleCounts != null) { int multisetHash = 0; @@ -292,7 +299,7 @@ public bool Equals(AnalysisMergeKey other) || !_realizationalFS.ValueEquals(other._realizationalFS) ) return false; - if (_finalTemplateState != other._finalTemplateState) + if (_finalTemplateState != other._finalTemplateState || _needsPartial != other._needsPartial) return false; return RuleCountsEqual(_ruleCounts, other._ruleCounts); } diff --git a/src/SIL.Machine.Morphology.HermitCrab/FinalTemplatePartialReachability.cs b/src/SIL.Machine.Morphology.HermitCrab/FinalTemplatePartialReachability.cs new file mode 100644 index 000000000..082ec09b2 --- /dev/null +++ b/src/SIL.Machine.Morphology.HermitCrab/FinalTemplatePartialReachability.cs @@ -0,0 +1,135 @@ +using System.Collections.Generic; +using System.Linq; +using SIL.Machine.FeatureModel; +using SIL.Machine.Morphology.HermitCrab.MorphologicalRules; + +namespace SIL.Machine.Morphology.HermitCrab +{ + /// + /// Finds the affix templates that a partial word can reach in synthesis. A word is partial at a template only + /// if a partial affix-process rule applied before it (a partial root takes no template), in the template's + /// stratum or an earlier one. Its part of speech is then whatever that rule leaves, changed by any rules + /// applied since. The search over-approximates: it tracks only the part-of-speech feature, treats an + /// unspecified or variable part of speech as every part of speech, ignores application counts, rule order, + /// template gating and every other feature, and lets every rule of a stratum apply any number of times. A + /// template it does not return can therefore never see a partial word. + /// + internal static class FinalTemplatePartialReachability + { + public static HashSet Compute(Language lang) + { + var result = new HashSet(); + SymbolicFeature posFeature = lang.SyntacticFeatureSystem.PartOfSpeechFeature; + HashSet all = + posFeature == null + ? new HashSet() + : new HashSet(posFeature.PossibleSymbols); + + // Part-of-speech values a partial word may carry on leaving the strata processed so far. + var reach = new HashSet(); + bool anyPartial = false; + foreach (Stratum stratum in lang.Strata) // synthesis order + { + List rules = stratum + .MorphologicalRules.Concat(stratum.AffixTemplates.SelectMany(t => t.Slots).SelectMany(s => s.Rules)) + .Distinct() + .ToList(); + + foreach (AffixProcessRule rule in rules.OfType().Where(r => r.IsPartial)) + { + anyPartial = true; + reach.UnionWith( + Apply(all, all, rule.RequiredSyntacticFeatureStruct, rule.OutSyntacticFeatureStruct) + ); + } + if (!anyPartial) + continue; + if (all.Count == 0) + { + // No parts of speech to tell templates apart: every later template is reachable. + result.UnionWith(stratum.AffixTemplates); + continue; + } + + // Close the set under every rule of this stratum. + bool changed = true; + while (changed) + { + changed = false; + foreach (IMorphologicalRule rule in rules) + { + if (reach.Count == 0) + break; + IEnumerable next = ApplyRule(all, reach, rule); + foreach (FeatureSymbol symbol in next.ToList()) + changed |= reach.Add(symbol); + } + } + + foreach (AffixTemplate template in stratum.AffixTemplates) + { + if (reach.Count == 0) + continue; + HashSet required = Constraint(all, template.RequiredSyntacticFeatureStruct); + if (required == null || required.Overlaps(reach)) + result.Add(template); + } + } + return result; + } + + private static IEnumerable ApplyRule( + HashSet all, + HashSet input, + IMorphologicalRule rule + ) + { + switch (rule) + { + case AffixProcessRule apr: + return Apply(all, input, apr.RequiredSyntacticFeatureStruct, apr.OutSyntacticFeatureStruct); + case CompoundingRule cr: + return Apply(all, input, cr.HeadRequiredSyntacticFeatureStruct, cr.OutSyntacticFeatureStruct); + case RealizationalAffixProcessRule rr: + return Apply(all, input, rr.RequiredSyntacticFeatureStruct, rr.RealizationalFeatureStruct); + default: + // Unknown rule kind: assume it can produce any part of speech. + return all; + } + } + + /// Unify with , then priority-union . + private static IEnumerable Apply( + HashSet all, + HashSet input, + FeatureStruct required, + FeatureStruct output + ) + { + HashSet req = Constraint(all, required); + bool applicable = req == null ? input.Count > 0 : req.Overlaps(input); + if (!applicable) + return Enumerable.Empty(); + HashSet outPos = Constraint(all, output); + if (outPos != null) + return outPos; + if (req == null) + return input; + return input.Where(req.Contains); + } + + /// The part-of-speech values a feature structure allows, or null if it does not constrain it. + private static HashSet Constraint(HashSet all, FeatureStruct fs) + { + if (fs == null) + return null; + if (!fs.TryGetValue(SyntacticFeatureSystem.PartOfSpeechID, out SymbolicFeatureValue value)) + return null; + if (value.IsVariable) + return null; + var symbols = new HashSet(value.Values); + // A value that names no known symbol is treated as unconstrained, to stay conservative. + return symbols.Count == 0 ? null : symbols; + } + } +} diff --git a/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs b/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs index 71fbb4fc4..de0335c08 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs @@ -68,6 +68,52 @@ public Morpher(ITraceManager traceManager, Language lang, int maxDegreeOfParalle _morphemes = new ReadOnlyObservableCollection(morphemes); HasPartialRules = GetPartialMorphemes().Any(morpheme => !(morpheme is LexEntry)); + if (HasPartialRules) + { + _partialReachableTemplates = FinalTemplatePartialReachability.Compute(lang); + _partialLookAhead = new PartialRuleLookAhead(lang); + _canPruneFinalTemplates = + _partialLookAhead.IsActive + || lang.Strata.SelectMany(s => s.AffixTemplates) + .Any(t => t.IsFinal && !_partialReachableTemplates.Contains(t)); + } + } + + private readonly HashSet _partialReachableTemplates; + private readonly PartialRuleLookAhead _partialLookAhead; + private readonly bool _canPruneFinalTemplates; + + /// + /// Whether analysis records the template state: the prune is on, or some final template can be pruned in a + /// grammar with partial rules. Otherwise the state would only split equivalent analyses. + /// + internal bool TracksFinalTemplateState => + !HasPartialRules || AlwaysEnforceFinalTemplates || _canPruneFinalTemplates; + + /// + /// Whether analysis may refuse to unapply a final template that follows a non-template rule. The synthesis + /// derivation this refuses has a non-partial rule after the final template, which is allowed only when the + /// word is partial there: a partial rule applied before the template. A partial rule leaves the analysis + /// state at none, so it never arms this. Templates no partial word can reach are pruned outright, and + /// others when the shape shows that no partial rule can still be unapplied. As with the prune it narrows, + /// a lexical family can make this drop a duplicate analysis: a family member that replaces the root is a + /// fresh word that forgets the pending final template. FieldWorks never writes a family; no distinct + /// analysis was lost in a grammar that has one. + /// + internal bool IsFinalTemplatePrunable(AffixTemplate template, Word input) + { + if (!HasPartialRules || AlwaysEnforceFinalTemplates) + return true; + return !_partialReachableTemplates.Contains(template) || !_partialLookAhead.PartialRuleMayUnapply(input); + } + + /// + /// Whether the analysis still owes a partial rule that its shape rules out for it and for anything derived + /// from it. + /// + internal bool OwesImpossiblePartial(Word input) + { + return input.NeedsPartial && !_partialLookAhead.PartialRuleMayUnapply(input); } public IEnumerable GetPartialMorphemes() @@ -103,8 +149,9 @@ public ITraceManager TraceManager public bool MergeEquivalentAnalyses { get; set; } /// - /// True if any morpheme other than a is partial. Analysis may prune a final - /// template that follows a non-template rule only when this is false or + /// True if any morpheme other than a is partial. Synthesis lets a non-template rule + /// follow a final template when the word or the rule is partial, so analysis prunes a final template that + /// follows a non-template rule only where no partial word can be involved, unless /// is set. /// public bool HasPartialRules { get; } @@ -355,6 +402,9 @@ private IEnumerable SynthesizeSequential(string word, IEnumerable an int alternativeCount = 0; foreach (Word analysisWord in analyses) { + // No partial rule was unapplied below the final template that followed a non-template rule. + if (analysisWord.NeedsPartial) + continue; foreach (Word synthesisWord in LexicalLookup(analysisWord)) { foreach (Word alternative in synthesisWord.ExpandAlternatives()) @@ -388,6 +438,8 @@ private IEnumerable SynthesizeParallel(string word, ConcurrentQueue for (int i = 0; i < range.Item2 - range.Item1; i++) { analyses.TryDequeue(out Word analysisWord); + if (analysisWord.NeedsPartial) + continue; foreach (Word synthesisWord in LexicalLookup(analysisWord)) { foreach (Word alternative in synthesisWord.ExpandAlternatives()) diff --git a/src/SIL.Machine.Morphology.HermitCrab/MorphologicalRules/AnalysisAffixProcessRule.cs b/src/SIL.Machine.Morphology.HermitCrab/MorphologicalRules/AnalysisAffixProcessRule.cs index cd036ee71..3c46a8784 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/MorphologicalRules/AnalysisAffixProcessRule.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/MorphologicalRules/AnalysisAffixProcessRule.cs @@ -115,14 +115,17 @@ out FeatureStruct syntacticFS outWord.SyntacticFeatureStruct = syntacticFS; } - if (!_morpher.HasPartialRules || _morpher.AlwaysEnforceFinalTemplates) + if (_morpher.TracksFinalTemplateState && !_rule.GetIsCliticRule()) { - if (!_rule.GetIsCliticRule()) - { - outWord.FinalTemplateState = !_rule.IsTemplateRule + // A partial rule may follow a final template in synthesis, so it never arms the prune. + // It also pays the debt of a final template unapplied after a non-template rule. + bool partialRule = !_rule.IsTemplateRule && _rule.IsPartial; + if (partialRule) + outWord.NeedsPartial = false; + outWord.FinalTemplateState = + !_rule.IsTemplateRule && (!partialRule || _morpher.AlwaysEnforceFinalTemplates) ? FinalTemplateState.NonTemplate : FinalTemplateState.None; - } } outWord.MorphologicalRuleUnapplied(_rule, _rule.Allomorphs[i]); outWord.Freeze(); diff --git a/src/SIL.Machine.Morphology.HermitCrab/PartialRuleLookAhead.cs b/src/SIL.Machine.Morphology.HermitCrab/PartialRuleLookAhead.cs new file mode 100644 index 000000000..9510d36c3 --- /dev/null +++ b/src/SIL.Machine.Morphology.HermitCrab/PartialRuleLookAhead.cs @@ -0,0 +1,262 @@ +using System.Collections.Generic; +using System.Linq; +using SIL.Machine.Annotations; +using SIL.Machine.FeatureModel; +using SIL.Machine.Matching; +using SIL.Machine.Morphology.HermitCrab.MorphologicalRules; + +namespace SIL.Machine.Morphology.HermitCrab +{ + /// + /// Decides from a word's shape whether analysis can still unapply a partial rule below it. Within a stratum + /// analysis unapplies the phonological rules once, on entry, and afterwards only morphological rules. When every + /// morphological rule of the stratum uses only and , + /// each node of every later shape is a clone of a node of the current shape, or a node + /// AnalysisMorphologicalTransform rebuilds from the pattern of a part that was deleted or can be empty. + /// A partial rule unapplies only if each segment it inserts unifies with a node of the shape at that time. The + /// look-ahead is off for a stratum when a rule can edit copied nodes (, + /// , an unknown rule type), when a partial rule lives in a deeper stratum + /// whose phonological rules would change the shape, or when a partial rule inserts no segment. + /// + internal sealed class PartialRuleLookAhead + { + private sealed class StratumMaterial + { + // Every rule of the stratum leaves a contiguous run of its input and rebuilds no node that a partial + // rule's inserted segment unifies with, so the inserted segments must already stand side by side. + public bool Contiguous; + + // Per partial-rule allomorph, the runs of segments it inserts. + public FeatureStruct[][][] Runs; + + // Per segment of those runs, whether a rule of the stratum can rebuild a node that it unifies with. + public bool[][][] Rebuildable; + } + + private sealed class Transform + { + // The parts whose nodes make up the unapplied shape. + public IList> Parts; + public IList Rhs; + } + + private readonly Dictionary _material = new Dictionary(); + + public PartialRuleLookAhead(Language lang) + { + bool partialInDeeperStratum = false; + foreach (Stratum stratum in lang.Strata) + { + AffixProcessRule[] partial = stratum + .MorphologicalRules.OfType() + .Where(r => r.IsPartial && !r.IsTemplateRule && !r.GetIsCliticRule()) + .ToArray(); + if (!partialInDeeperStratum && partial.Length > 0) + _material[stratum] = Inspect(stratum, partial); + partialInDeeperStratum |= partial.Length > 0; + } + } + + /// + /// False when no partial rule can be unapplied to this word or to anything analysis derives from it in + /// its stratum. + /// + public bool PartialRuleMayUnapply(Word input) + { + if (!_material.TryGetValue(input.Stratum, out StratumMaterial material) || material == null) + return true; + ShapeNode[] nodes = input.Shape.Where(n => n.Annotation.Type() == HCFeatureSystem.Segment).ToArray(); + if (nodes.Length != input.Shape.Count) + return true; + for (int a = 0; a < material.Runs.Length; a++) + { + bool possible = true; + for (int r = 0; possible && r < material.Runs[a].Length; r++) + { + FeatureStruct[] run = material.Runs[a][r]; + if (material.Contiguous) + { + possible = RunOccurs(nodes, run); + continue; + } + for (int i = 0; possible && i < run.Length; i++) + { + possible = + material.Rebuildable[a][r][i] + || nodes.Any(n => n.Annotation.FeatureStruct.IsUnifiable(run[i], false, null)); + } + } + if (possible) + return true; + } + return false; + } + + public bool IsActive => _material.Values.Any(m => m != null); + + private static StratumMaterial Inspect(Stratum stratum, AffixProcessRule[] partial) + { + IEnumerable rules = stratum + .MorphologicalRules.Concat(stratum.AffixTemplates.SelectMany(t => t.Slots).SelectMany(s => s.Rules)) + .Distinct(); + var rebuilt = new List(); + int notContiguous = 0; + foreach (IMorphologicalRule rule in rules) + { + IEnumerable transforms = Transforms(rule); + if (transforms == null) + return null; + foreach (Transform t in transforms) + { + if (!Inspect(t, rebuilt, out bool contiguous)) + return null; + if (!contiguous) + notContiguous++; + } + } + + FeatureStruct[][][] runs = partial + .SelectMany(r => r.Allomorphs) + .Select(a => + a.Rhs.OfType() + .Select(ins => + ins.Segments.Shape.Where(n => n.Annotation.Type() == HCFeatureSystem.Segment) + .Select(n => n.Annotation.FeatureStruct) + .ToArray() + ) + .Where(run => run.Length > 0) + .ToArray() + ) + .ToArray(); + // An allomorph that inserts nothing can be unapplied to any shape. + if (runs.Any(allomorphRuns => allomorphRuns.Length == 0)) + return null; + + bool[][][] rebuildable = runs.Select(allomorphRuns => + allomorphRuns + .Select(run => + run.Select(fs => rebuilt.Any(c => c.HasVariables || c.IsUnifiable(fs, false, null))) + .ToArray() + ) + .ToArray() + ) + .ToArray(); + return new StratumMaterial + { + Runs = runs, + Rebuildable = rebuildable, + Contiguous = notContiguous == 0 && !rebuildable.Any(a => a.Any(r => r.Any(x => x))), + }; + } + + private static IEnumerable Transforms(IMorphologicalRule rule) + { + switch (rule) + { + case AffixProcessRule ap: + return ap.Allomorphs.Select(a => new Transform { Parts = a.Lhs, Rhs = a.Rhs }); + case RealizationalAffixProcessRule rp: + return rp.Allomorphs.Select(a => new Transform { Parts = a.Lhs, Rhs = a.Rhs }); + case CompoundingRule cr: + return cr.Subrules.Select(sr => new Transform { Parts = sr.HeadLhs, Rhs = sr.Rhs }); + default: + return null; + } + } + + // Unapplying the transform keeps a contiguous run of the input's nodes, in order, and rebuilds only the + // nodes added to "rebuilt". Returns false if it can do anything else. + private static bool Inspect(Transform t, List rebuilt, out bool contiguous) + { + contiguous = false; + if (t.Rhs.Any(a => !(a is CopyFromInput) && !(a is InsertSegments))) + return false; + List names = t.Parts.Select(p => p.Name).ToList(); + var copies = new List(); + bool insertedAfterCopy = false; + bool insertedBetween = false; + foreach (MorphologicalOutputAction action in t.Rhs) + { + if (action is CopyFromInput) + { + int index = names.IndexOf(action.PartName); + if (index < 0) + continue; // a non-head part of a compound + if (insertedAfterCopy) + insertedBetween = true; + copies.Add(index); + } + else if (copies.Count > 0) + { + insertedAfterCopy = true; + } + } + for (int i = 0; i < names.Count; i++) + { + if (!copies.Contains(i) || !NeverEmpty(t.Parts[i])) + CollectSegments(t.Parts[i], rebuilt); + } + bool increasing = true; + for (int i = 1; i < copies.Count; i++) + increasing &= copies[i] > copies[i - 1]; + contiguous = copies.Distinct().Count() <= 1 || (increasing && !insertedBetween); + return true; + } + + // A group over this part always captures at least one node. + private static bool NeverEmpty(PatternNode node) + { + foreach (PatternNode child in node.Children) + { + if (child is Constraint c && c.Type() == HCFeatureSystem.Segment) + return true; + if (child is Quantifier q) + { + if (q.MinOccur >= 1 && NeverEmpty(q)) + return true; + } + else if (child is Group g && NeverEmpty(g)) + { + return true; + } + } + return false; + } + + // Mirrors AnalysisMorphologicalTransform.Untruncate: an unbounded quantifier rebuilds nothing. + private static void CollectSegments(PatternNode node, List into) + { + if (node is Constraint c && c.Type() == HCFeatureSystem.Segment) + into.Add(c.FeatureStruct); + if (node is Quantifier q && q.MaxOccur <= 0) + return; + foreach (PatternNode child in node.Children) + CollectSegments(child, into); + } + + private static bool RunOccurs(ShapeNode[] nodes, FeatureStruct[] run) + { + for (int start = 0; start < nodes.Length; start++) + { + if (RunMatches(nodes, start, run, 0)) + return true; + } + return false; + } + + // Optional nodes may be passed over once the run has started. + private static bool RunMatches(ShapeNode[] nodes, int n, FeatureStruct[] run, int r) + { + if (r == run.Length) + return true; + if (n == nodes.Length) + return false; + if ( + nodes[n].Annotation.FeatureStruct.IsUnifiable(run[r], false, null) + && RunMatches(nodes, n + 1, run, r + 1) + ) + return true; + return r > 0 && nodes[n].Annotation.Optional && RunMatches(nodes, n + 1, run, r); + } + } +} diff --git a/src/SIL.Machine.Morphology.HermitCrab/Word.cs b/src/SIL.Machine.Morphology.HermitCrab/Word.cs index 43b0e04c9..b3ab02fc5 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/Word.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/Word.cs @@ -31,6 +31,7 @@ public class Word : Freezable, IAnnotatedData, ICloneable private bool? _isLastAppliedRuleFinal; private FinalTemplateState _finalTemplateState; private bool _isPartial; + private bool _needsPartial; private readonly Dictionary> _disjunctiveAllomorphIndices; private int _mruleAppCount = 0; private readonly IList _alternatives = new List(); @@ -97,6 +98,7 @@ protected Word(Word word) _isLastAppliedRuleFinal = word._isLastAppliedRuleFinal; _finalTemplateState = word._finalTemplateState; _isPartial = word._isPartial; + _needsPartial = word._needsPartial; CurrentTrace = word.CurrentTrace; _disjunctiveAllomorphIndices = word._disjunctiveAllomorphIndices.ToDictionary( kvp => kvp.Key, @@ -418,6 +420,20 @@ internal bool? IsLastAppliedRuleFinal } } + /// + /// Analysis unapplied a final template right after a non-template rule. Synthesis allows that only if a + /// partial rule applied before the template, so one must still be unapplied for the analysis to match. + /// + internal bool NeedsPartial + { + get { return _needsPartial; } + set + { + CheckFrozen(); + _needsPartial = value; + } + } + internal FinalTemplateState FinalTemplateState { get { return _finalTemplateState; } @@ -588,6 +604,7 @@ protected override int FreezeImpl() code = code * 31 + _mruleAppIndex.GetHashCode(); code = code * 31 + _isLastAppliedRuleFinal.GetHashCode(); code = code * 31 + _finalTemplateState.GetHashCode(); + code = code * 31 + (_needsPartial ? 1 : 0); return code; } @@ -612,7 +629,8 @@ public override bool ValueEquals(Word other) && _mruleApps.SequenceEqual(other._mruleApps) && _mruleAppIndex == other._mruleAppIndex && _isLastAppliedRuleFinal == other._isLastAppliedRuleFinal - && _finalTemplateState == other._finalTemplateState; + && _finalTemplateState == other._finalTemplateState + && _needsPartial == other._needsPartial; } public Word Clone() From 919484dde8858b33c4c32b1e410b95a11b8b11d5 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Thu, 12 Nov 2026 16:34:52 -0500 Subject: [PATCH 2/8] Test the partial-rule prune against master's analyses and fix two defects the tests found The tests parse 19 adversarial grammars at degree 1 and the default degree and compare the allomorph sets with pure master, bound the work on three grammars, and guard the set of output action types the look-ahead knows. They exposed two defects. With final-template state tracking off, a compounding rule set the state and the template rule then owed a partial nobody cleared (grammar g21). The look-ahead unified against unfrozen feature structures from several threads and lost parses at the default degree, so it unifies against frozen copies. Co-Authored-By: Claude Opus 5.5 --- .../AnalysisAffixTemplateRule.cs | 5 +- .../PartialRuleLookAhead.cs | 12 +- .../AffixTemplateTests.cs | 2 +- .../PartialRulePruneTests.cs | 156 ++++++++++++++++++ .../TestData/PartialPrune/g10.expected | 13 ++ .../TestData/PartialPrune/g10.words | 13 ++ .../TestData/PartialPrune/g10.xml | 37 +++++ .../TestData/PartialPrune/g11.expected | 21 +++ .../TestData/PartialPrune/g11.words | 21 +++ .../TestData/PartialPrune/g11.xml | 48 ++++++ .../TestData/PartialPrune/g12.expected | 19 +++ .../TestData/PartialPrune/g12.words | 19 +++ .../TestData/PartialPrune/g12.xml | 40 +++++ .../TestData/PartialPrune/g13.expected | 18 ++ .../TestData/PartialPrune/g13.words | 18 ++ .../TestData/PartialPrune/g13.xml | 40 +++++ .../TestData/PartialPrune/g13b.expected | 17 ++ .../TestData/PartialPrune/g13b.words | 17 ++ .../TestData/PartialPrune/g13b.xml | 40 +++++ .../TestData/PartialPrune/g13c.expected | 7 + .../TestData/PartialPrune/g13c.words | 7 + .../TestData/PartialPrune/g13c.xml | 35 ++++ .../TestData/PartialPrune/g14.expected | 17 ++ .../TestData/PartialPrune/g14.words | 17 ++ .../TestData/PartialPrune/g14.xml | 40 +++++ .../TestData/PartialPrune/g15.expected | 18 ++ .../TestData/PartialPrune/g15.words | 18 ++ .../TestData/PartialPrune/g15.xml | 40 +++++ .../TestData/PartialPrune/g16.expected | 18 ++ .../TestData/PartialPrune/g16.words | 18 ++ .../TestData/PartialPrune/g16.xml | 42 +++++ .../TestData/PartialPrune/g17.expected | 13 ++ .../TestData/PartialPrune/g17.words | 13 ++ .../TestData/PartialPrune/g17.xml | 41 +++++ .../TestData/PartialPrune/g18.expected | 13 ++ .../TestData/PartialPrune/g18.words | 13 ++ .../TestData/PartialPrune/g18.xml | 41 +++++ .../TestData/PartialPrune/g19.expected | 16 ++ .../TestData/PartialPrune/g19.words | 16 ++ .../TestData/PartialPrune/g19.xml | 42 +++++ .../TestData/PartialPrune/g19c.expected | 6 + .../TestData/PartialPrune/g19c.words | 6 + .../TestData/PartialPrune/g19c.xml | 42 +++++ .../TestData/PartialPrune/g20.expected | 17 ++ .../TestData/PartialPrune/g20.words | 17 ++ .../TestData/PartialPrune/g20.xml | 41 +++++ .../TestData/PartialPrune/g21.expected | 18 ++ .../TestData/PartialPrune/g21.words | 18 ++ .../TestData/PartialPrune/g21.xml | 49 ++++++ .../TestData/PartialPrune/g6.expected | 17 ++ .../TestData/PartialPrune/g6.words | 17 ++ .../TestData/PartialPrune/g6.xml | 45 +++++ .../TestData/PartialPrune/g6b.expected | 10 ++ .../TestData/PartialPrune/g6b.words | 10 ++ .../TestData/PartialPrune/g6b.xml | 49 ++++++ .../TestData/PartialPrune/g7.expected | 8 + .../TestData/PartialPrune/g7.words | 8 + .../TestData/PartialPrune/g7.xml | 38 +++++ .../TestData/PartialPrune/g8.expected | 24 +++ .../TestData/PartialPrune/g8.words | 24 +++ .../TestData/PartialPrune/g8.xml | 59 +++++++ 61 files changed, 1560 insertions(+), 4 deletions(-) create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/PartialRulePruneTests.cs create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g10.expected create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g10.words create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g10.xml create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g11.expected create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g11.words create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g11.xml create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g12.expected create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g12.words create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g12.xml create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13.expected create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13.words create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13.xml create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13b.expected create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13b.words create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13b.xml create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13c.expected create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13c.words create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13c.xml create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g14.expected create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g14.words create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g14.xml create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g15.expected create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g15.words create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g15.xml create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g16.expected create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g16.words create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g16.xml create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g17.expected create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g17.words create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g17.xml create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g18.expected create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g18.words create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g18.xml create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g19.expected create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g19.words create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g19.xml create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g19c.expected create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g19c.words create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g19c.xml create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g20.expected create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g20.words create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g20.xml create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g21.expected create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g21.words create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g21.xml create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g6.expected create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g6.words create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g6.xml create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g6b.expected create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g6b.words create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g6b.xml create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g7.expected create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g7.words create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g7.xml create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g8.expected create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g8.words create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g8.xml diff --git a/src/SIL.Machine.Morphology.HermitCrab/AnalysisAffixTemplateRule.cs b/src/SIL.Machine.Morphology.HermitCrab/AnalysisAffixTemplateRule.cs index 183c2da57..e4dcb160f 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/AnalysisAffixTemplateRule.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/AnalysisAffixTemplateRule.cs @@ -42,7 +42,10 @@ public IEnumerable Apply(Word input) _morpher.TraceManager.BeginUnapplyTemplate(_template, input); // In synthesis a non-template rule follows a final template only if a partial rule applied earlier. - bool afterNonTemplate = _template.IsFinal && input.FinalTemplateState == FinalTemplateState.NonTemplate; + bool afterNonTemplate = + _morpher.TracksFinalTemplateState + && _template.IsFinal + && input.FinalTemplateState == FinalTemplateState.NonTemplate; if (afterNonTemplate && _morpher.IsFinalTemplatePrunable(_template, input)) { if (_morpher.TraceManager.IsTracing) diff --git a/src/SIL.Machine.Morphology.HermitCrab/PartialRuleLookAhead.cs b/src/SIL.Machine.Morphology.HermitCrab/PartialRuleLookAhead.cs index 9510d36c3..99852f1b3 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/PartialRuleLookAhead.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/PartialRuleLookAhead.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using SIL.Machine.Annotations; using SIL.Machine.FeatureModel; @@ -121,7 +121,7 @@ private static StratumMaterial Inspect(Stratum stratum, AffixProcessRule[] parti a.Rhs.OfType() .Select(ins => ins.Segments.Shape.Where(n => n.Annotation.Type() == HCFeatureSystem.Segment) - .Select(n => n.Annotation.FeatureStruct) + .Select(n => FrozenCopy(n.Annotation.FeatureStruct)) .ToArray() ) .Where(run => run.Length > 0) @@ -149,6 +149,14 @@ private static StratumMaterial Inspect(Stratum stratum, AffixProcessRule[] parti }; } + // Look-ahead runs on several threads, and unifying with an unfrozen structure is not thread-safe. + private static FeatureStruct FrozenCopy(FeatureStruct fs) + { + FeatureStruct copy = fs.Clone(); + copy.Freeze(); + return copy; + } + private static IEnumerable Transforms(IMorphologicalRule rule) { switch (rule) diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/AffixTemplateTests.cs b/tests/SIL.Machine.Morphology.HermitCrab.Tests/AffixTemplateTests.cs index 83498f566..fae09c598 100644 --- a/tests/SIL.Machine.Morphology.HermitCrab.Tests/AffixTemplateTests.cs +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/AffixTemplateTests.cs @@ -555,7 +555,7 @@ AffixProcessRule CreateSuffix(string name, string gloss, string suffix) AssertMorphsEqual(morpher.ParseWord("sabdv")); AssertMorphsEqual(morpher.ParseWord("sabv"), "partial_root NOM"); - // A partial rule still turns the prune off, and what it licenses after a final template survives. + // A partial rule leaves the state at none, so what it licenses after a final template survives. nominalizer.IsPartial = true; morpher = new Morpher(TraceManager, Language); Assert.That(morpher.HasPartialRules, Is.True); diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/PartialRulePruneTests.cs b/tests/SIL.Machine.Morphology.HermitCrab.Tests/PartialRulePruneTests.cs new file mode 100644 index 000000000..7c381edfb --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/PartialRulePruneTests.cs @@ -0,0 +1,156 @@ +using NUnit.Framework; +using SIL.Machine.Morphology.HermitCrab.MorphologicalRules; + +namespace SIL.Machine.Morphology.HermitCrab; + +// The grammars in TestData/PartialPrune put partial material where a careless prune of final templates would miss +// it. The .expected files hold the analyses (allomorphs, shape, syntactic features) found before +// the prune was narrowed, so a lost, extra or changed analysis fails a test. +public class PartialRulePruneTests +{ + private static readonly string[] Grammars = + [ + "g6", // partial rules with no category + "g6b", // partial rules in a deeper stratum than the template + "g7", // a slot rule shared by a final and a non-final template + "g8", // final templates a partial rule can reach and ones it cannot + "g10", // plain prefixes and suffixes around the partial material + "g11", // partial material wrapped by affixes and copied by reduplication + "g12", // an infix splits the partial material + "g13", // truncation replaces the partial rule's last segment + "g13b", // truncation of the partial prefix's first segment + "g13c", // truncation replaces the partial rule's last segment, minimal + "g14", // a copied part that can be empty + "g15", // two parts copied in swapped order move the material apart + "g16", // compounding outside the partial rule + "g17", // partial rules in a deeper stratum + "g18", // partial rules in both strata + "g19", // phonological deletion hides a partial segment + "g19c", // phonological deletion hides a partial segment, minimal + "g20", // three partial rules + "g21", // compounding and a partial rule the look-ahead cannot reason about + ]; + + // Grammars whose words need the deletion rules to be reapplied. + private static readonly HashSet DeletionReapplied = ["g19", "g19c"]; + + private static string DataPath(string file) => + Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "TestData", "PartialPrune", file); + + private static IEnumerable GrammarCases() + { + foreach (string grammar in Grammars) + { + yield return new TestCaseData(grammar, 1).SetName($"{grammar}_Degree1"); + yield return new TestCaseData(grammar, 0).SetName($"{grammar}_DefaultDegree"); + } + } + + private static Morpher CreateMorpher( + string grammar, + int maxDegreeOfParallelism, + Func? selector = null + ) + { + Language language = XmlLanguageLoader.Load(DataPath(grammar + ".xml")); + var morpher = new Morpher(new TraceManager(), language, maxDegreeOfParallelism); + if (DeletionReapplied.Contains(grammar)) + morpher.DeletionReapplications = 1; + if (selector != null) + morpher.RuleSelector = selector; + return morpher; + } + + // Allomorphs, not AllomorphsInMorphOrder: morph order comes from shape annotations, which a truncation can drop. + // Distinct: the number of identical analyses a parse returns varies between runs. + private static string Signature(Morpher morpher, string word) + { + try + { + List analyses = morpher + .ParseWord(word) + .Select(w => + string.Join( + "+", + w.Allomorphs.Select(a => $"{a.Morpheme.Id}.{a.Index}").OrderBy(n => n, StringComparer.Ordinal) + ) + + "|" + + w.Shape.ToRegexString(w.Stratum.CharacterDefinitionTable, true) + + "|" + + w.SyntacticFeatureStruct + ) + .Distinct() + .OrderBy(s => s, StringComparer.Ordinal) + .ToList(); + return analyses.Count == 0 ? "-" : string.Join(";", analyses); + } + catch (InvalidShapeException) + { + return "SKIPPED"; + } + } + + [TestCaseSource(nameof(GrammarCases))] + public void ParseWord_PartialRuleGrammar_KeepsEveryAnalysis(string grammar, int maxDegreeOfParallelism) + { + Morpher morpher = CreateMorpher(grammar, maxDegreeOfParallelism); + Assert.That(morpher.HasPartialRules, Is.True); + + var differences = new List(); + + foreach (string line in File.ReadAllLines(DataPath(grammar + ".expected"))) + { + string[] fields = line.Split('\t'); + string actual = Signature(morpher, fields[0]); + if (actual != fields[1]) + differences.Add($"{fields[0]}: expected {fields[1]} but was {actual}"); + } + Assert.That(differences, Is.Empty); + } + + // Rule-selector calls are the work counter: every analysis and synthesis rule asks once per application. With + // one thread the count is deterministic, so a bound separates a prune that fires from one that does not. + [TestCase("g8", "akamdt", 130, TestName = "TemplateNoPartialWordCanReach")] + [TestCase("g6", "loptr", 18, TestName = "ShapeHasNoRoomForPartialMaterialContiguous")] + [TestCase("g12", "loptr", 16, TestName = "ShapeHasNoRoomForPartialMaterialExistence")] + public void ParseWord_PartialRuleGrammar_PrunesWhatNoPartialWordCanUse(string grammar, string word, int maxCalls) + { + int calls = 0; + Morpher morpher = CreateMorpher( + grammar, + maxDegreeOfParallelism: 1, + selector: _ => + { + calls++; + return true; + } + ); + Signature(morpher, word); + Assert.That(calls, Is.LessThanOrEqualTo(maxCalls)); + } + + // The look-ahead assumes that unapplying a rule copies nodes of its input unchanged and adds only nodes it + // inserts or rebuilds. A new kind of output action has to be considered there before it is listed here. + [Test] + public void MorphologicalOutputAction_Subtypes_AreTheOnesPartialRuleLookAheadKnows() + { + string[] subtypes = typeof(MorphologicalOutputAction) + .Assembly.GetTypes() + .Where(t => t.IsSubclassOf(typeof(MorphologicalOutputAction))) + .Select(t => t.Name) + .OrderBy(n => n, StringComparer.Ordinal) + .ToArray(); + Assert.That( + subtypes, + Is.EqualTo( + new[] + { + nameof(CopyFromInput), + nameof(InsertSegments), + nameof(InsertSimpleContext), + nameof(ModifyFromInput), + } + ) + ); + } +} diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g10.expected b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g10.expected new file mode 100644 index 000000000..b583aa9ed --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g10.expected @@ -0,0 +1,13 @@ +lopkotr LOP.0+PK.0+R.0+T.0|lopkotr|[POS:B] +loptr - +lopkot LOP.0+PK.0+T.0|lopkot|[POS:B] +lopt LOP.0+T.0|lopt|[POS:B] +lopktr - +lopotr - +loptkor LOP.0+PK.0+R.0+T.0|loptkor|[POS:B] +lopkotkor - +kaloptr LOP.0+PC.0+R.0+T.0|kaloptr|[POS:B] +kalopkotr LOP.0+PC.0+PK.0+R.0+T.0|kalopkotr|[POS:B] +loptrko - +loptkr - +lopkor LOP.0+PK.0+R.0|lopkor|[POS:B] diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g10.words b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g10.words new file mode 100644 index 000000000..e7afb8792 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g10.words @@ -0,0 +1,13 @@ +lopkotr +loptr +lopkot +lopt +lopktr +lopotr +loptkor +lopkotkor +kaloptr +kalopkotr +loptrko +loptkr +lopkor diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g10.xml b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g10.xml new file mode 100644 index 000000000..ba73f655f --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g10.xml @@ -0,0 +1,37 @@ + + + + LaPlain + ABE + Main +abdegiklmnopqrstuwxyz + + Any + +Main +pk + +ko +PKpk +pc + +ka +PCpc +t + +t +Tt +r + +r +Rr + +T1slotT1 + +lopLOPlop +kamKAMkam +moMOmo + + + + diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g11.expected b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g11.expected new file mode 100644 index 000000000..cbfb7b00a --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g11.expected @@ -0,0 +1,21 @@ +lopkotr LOP.0+PK.0+R.0+T.0|lopkotr|[POS:B] +loptr - +lopkot LOP.0+PK.0+T.0|lopkot|[POS:B] +lopt LOP.0+T.0|lopt|[POS:B] +lopktr - +lopotr - +loptkor LOP.0+PK.0+R.0+T.0|loptkor|[POS:B] +lopkotkor - +kaloptr LOP.0+PC.0+R.0+T.0|kaloptr|[POS:B] +kalopkotr LOP.0+PC.0+PK.0+R.0+T.0|kalopkotr|[POS:B] +loptrko - +loptkr - +lopkor LOP.0+PK.0+R.0|lopkor|[POS:B] +lopkontr LOP.0+N.0+PK.0+R.0+T.0|lopkontr|[POS:B] +milopkotr LOP.0+M.0+PK.0+R.0+T.0|milopkotr|[POS:B] +mikaloptr LOP.0+M.0+PC.0+R.0+T.0|mikaloptr|[POS:B] +lopkolopkotr LOP.0+PK.0+R.0+RD.0+T.0|lopkolopkotr|[POS:B] +kalopkaloptr LOP.0+PC.0+R.0+RD.0+T.0|kalopkaloptr|[POS:B] +lopkonnt - +lopnkotr LOP.0+N.0+PK.0+R.0+T.0|lopnkotr|[POS:B] +lopkolopkontr LOP.0+N.0+PK.0+R.0+RD.0+T.0|lopkolopkontr|[POS:B] diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g11.words b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g11.words new file mode 100644 index 000000000..6fb91f13a --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g11.words @@ -0,0 +1,21 @@ +lopkotr +loptr +lopkot +lopt +lopktr +lopotr +loptkor +lopkotkor +kaloptr +kalopkotr +loptrko +loptkr +lopkor +lopkontr +milopkotr +mikaloptr +lopkolopkotr +kalopkaloptr +lopkonnt +lopnkotr +lopkolopkontr diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g11.xml b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g11.xml new file mode 100644 index 000000000..c031297f9 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g11.xml @@ -0,0 +1,48 @@ + + + + LaWrap + ABE + Main +abdegiklmnopqrstuwxyz + + Any + +Main +pk + +ko +PKpk +pc + +ka +PCpc +t + +t +Tt +r + +r +Rr +n + +n +Nn +m + +mi +Mm +rd + +RDrd + +T1slotT1 + +lopLOPlop +kamKAMkam +moMOmo + + + + diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g12.expected b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g12.expected new file mode 100644 index 000000000..26bef54a8 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g12.expected @@ -0,0 +1,19 @@ +lopkotr LOP.0+PK.0+R.0+T.0|lopkotr|[POS:B] +loptr - +lopkot LOP.0+PK.0+T.0|lopkot|[POS:B] +lopt LOP.0+T.0|lopt|[POS:B] +lopktr - +lopotr - +loptkor LOP.0+PK.0+R.0+T.0|loptkor|[POS:B] +lopkotkor - +kaloptr LOP.0+PC.0+R.0+T.0|kaloptr|[POS:B] +kalopkotr LOP.0+PC.0+PK.0+R.0+T.0|kalopkotr|[POS:B] +loptrko - +loptkr - +lopkor LOP.0+PK.0+R.0|lopkor|[POS:B] +lopkxotr IX.0+LOP.0+PK.0+R.0+T.0|lopkxotr|[POS:B] +lopkxot IX.0+LOP.0+PK.0+T.0|lopkxot|[POS:B] +loxptr - +lopxtr - +kalxoptr - +lopkxoxtr - diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g12.words b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g12.words new file mode 100644 index 000000000..b89db220d --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g12.words @@ -0,0 +1,19 @@ +lopkotr +loptr +lopkot +lopt +lopktr +lopotr +loptkor +lopkotkor +kaloptr +kalopkotr +loptrko +loptkr +lopkor +lopkxotr +lopkxot +loxptr +lopxtr +kalxoptr +lopkxoxtr diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g12.xml b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g12.xml new file mode 100644 index 000000000..75e6675ec --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g12.xml @@ -0,0 +1,40 @@ + + + + LaInfix + ABE + Main +abdegiklmnopqrstuwxyz + + Any + +Main +pk + +ko +PKpk +pc + +ka +PCpc +t + +t +Tt +r + +r +Rr +ix +x +IXix + +T1slotT1 + +lopLOPlop +kamKAMkam +moMOmo + + + + diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13.expected b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13.expected new file mode 100644 index 000000000..d90cbf496 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13.expected @@ -0,0 +1,18 @@ +lopkotr LOP.0+PK.0+R.0+T.0|lopkotr|[POS:B] +loptr - +lopkot LOP.0+PK.0+T.0|lopkot|[POS:B] +lopt LOP.0+T.0|lopt|[POS:B] +lopktr - +lopotr - +loptkor LOP.0+PK.0+R.0+T.0|loptkor|[POS:B] +lopkotkor - +kaloptr LOP.0+PC.0+R.0+T.0|kaloptr|[POS:B] +kalopkotr LOP.0+PC.0+PK.0+R.0+T.0|kalopkotr|[POS:B] +loptrko - +loptkr - +lopkor LOP.0+PK.0+R.0|lopkor|[POS:B] +lopkatr LOP.0+PK.0+R.0+T.0+TR.0|lopkatr|[POS:B] +lopkat LOP.0+PK.0+T.0+TR.0|lopkat|[POS:B] +lopatr - +loptr - +lopkaktr - diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13.words b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13.words new file mode 100644 index 000000000..52b2a855f --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13.words @@ -0,0 +1,18 @@ +lopkotr +loptr +lopkot +lopt +lopktr +lopotr +loptkor +lopkotkor +kaloptr +kalopkotr +loptrko +loptkr +lopkor +lopkatr +lopkat +lopatr +loptr +lopkaktr diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13.xml b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13.xml new file mode 100644 index 000000000..d4bff2e9f --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13.xml @@ -0,0 +1,40 @@ + + + + LaTrunc + ABE + Main +abdegiklmnopqrstuwxyz + + Any + +Main +pk + +ko +PKpk +pc + +ka +PCpc +t + +t +Tt +r + +r +Rr +tr +a +TRtr + +T1slotT1 + +lopLOPlop +kamKAMkam +moMOmo + + + + diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13b.expected b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13b.expected new file mode 100644 index 000000000..fdd3e80fe --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13b.expected @@ -0,0 +1,17 @@ +lopkotr LOP.0+PK.0+R.0+T.0|lopkotr|[POS:B] +loptr - +lopkot LOP.0+PK.0+T.0|lopkot|[POS:B] +lopt LOP.0+T.0|lopt|[POS:B] +lopktr - +lopotr - +loptkor LOP.0+PK.0+R.0+T.0|loptkor|[POS:B] +lopkotkor - +kaloptr LOP.0+PC.0+R.0+T.0|kaloptr|[POS:B] +kalopkotr LOP.0+PC.0+PK.0+R.0+T.0|kalopkotr|[POS:B] +loptrko - +loptkr - +lopkor LOP.0+PK.0+R.0|lopkor|[POS:B] +aloptr LOP.0+PC.0+R.0+T.0+TR.0|aloptr|[POS:B] +alopt LOP.0+PC.0+T.0+TR.0|alopt|[POS:B] +oloptr - +lopkotr LOP.0+PK.0+R.0+T.0|lopkotr|[POS:B] diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13b.words b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13b.words new file mode 100644 index 000000000..5a2fc13d4 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13b.words @@ -0,0 +1,17 @@ +lopkotr +loptr +lopkot +lopt +lopktr +lopotr +loptkor +lopkotkor +kaloptr +kalopkotr +loptrko +loptkr +lopkor +aloptr +alopt +oloptr +lopkotr diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13b.xml b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13b.xml new file mode 100644 index 000000000..7fc17c873 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13b.xml @@ -0,0 +1,40 @@ + + + + LaTruncPre + ABE + Main +abdegiklmnopqrstuwxyz + + Any + +Main +pk + +ko +PKpk +pc + +ka +PCpc +t + +t +Tt +r + +r +Rr +tr + +TRtr + +T1slotT1 + +lopLOPlop +kamKAMkam +moMOmo + + + + diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13c.expected b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13c.expected new file mode 100644 index 000000000..4276161cc --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13c.expected @@ -0,0 +1,7 @@ +lipkatr LIP.0+PK.0+R.0+T.0+TR.0|lipkatr|[POS:B] +lipkat LIP.0+PK.0+T.0+TR.0|lipkat|[POS:B] +lipatr - +liptr - +lipkotr LIP.0+PK.0+R.0+T.0|lipkotr|[POS:B] +lipkaktr - +kalipatr - diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13c.words b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13c.words new file mode 100644 index 000000000..dcb87d2e4 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13c.words @@ -0,0 +1,7 @@ +lipkatr +lipkat +lipatr +liptr +lipkotr +lipkaktr +kalipatr diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13c.xml b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13c.xml new file mode 100644 index 000000000..9e0d36045 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g13c.xml @@ -0,0 +1,35 @@ + + + + LaTrunc2 + ABE + Main +abdegiklmnopqrstuwxyz + + Any + +Main +pk + +ko +PKpk +t + +t +Tt +r + +r +Rr +tr +a +TRtr + +T1slotT1 + +lipLIPlip +moMOmo + + + + diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g14.expected b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g14.expected new file mode 100644 index 000000000..651f53a1a --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g14.expected @@ -0,0 +1,17 @@ +lopkotr LOP.0+PK.0+R.0+T.0|lopkotr|[POS:B] +loptr - +lopkot LOP.0+PK.0+T.0|lopkot|[POS:B] +lopt LOP.0+T.0|lopt|[POS:B] +lopktr - +lopotr - +loptkor LOP.0+PK.0+R.0+T.0|loptkor|[POS:B] +lopkotkor - +kaloptr LOP.0+PC.0+R.0+T.0|kaloptr|[POS:B] +kalopkotr LOP.0+PC.0+PK.0+R.0+T.0|kalopkotr|[POS:B] +loptrko - +loptkr - +lopkor LOP.0+PK.0+R.0|lopkor|[POS:B] +lopkontr LOP.0+OQ.0+PK.0+R.0+T.0|lopkontr|[POS:B] +lopkntr - +lopntr - +lopknotr - diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g14.words b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g14.words new file mode 100644 index 000000000..39a6b9882 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g14.words @@ -0,0 +1,17 @@ +lopkotr +loptr +lopkot +lopt +lopktr +lopotr +loptkor +lopkotkor +kaloptr +kalopkotr +loptrko +loptkr +lopkor +lopkontr +lopkntr +lopntr +lopknotr diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g14.xml b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g14.xml new file mode 100644 index 000000000..0d8133c2e --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g14.xml @@ -0,0 +1,40 @@ + + + + LaOptPart + ABE + Main +abdegiklmnopqrstuwxyz + + Any + +Main +pk + +ko +PKpk +pc + +ka +PCpc +t + +t +Tt +r + +r +Rr +oq +n +OQoq + +T1slotT1 + +lopLOPlop +kamKAMkam +moMOmo + + + + diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g15.expected b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g15.expected new file mode 100644 index 000000000..0bc1ed36c --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g15.expected @@ -0,0 +1,18 @@ +lopkotr LOP.0+PK.0+R.0+T.0|lopkotr|[POS:B] +loptr - +lopkot LOP.0+PK.0+T.0|lopkot|[POS:B] +lopt LOP.0+T.0|lopt|[POS:B] +lopktr - +lopotr - +loptkor LOP.0+PK.0+R.0+T.0|loptkor|[POS:B] +lopkotkor - +kaloptr LOP.0+PC.0+R.0+T.0|kaloptr|[POS:B] +kalopkotr LOP.0+PC.0+PK.0+R.0+T.0|kalopkotr|[POS:B] +loptrko - +loptkr - +lopkor LOP.0+PK.0+R.0|lopkor|[POS:B] +olopktr LOP.0+PK.0+R.0+SW.0+T.0|olopktr|[POS:B] +olopkt LOP.0+PK.0+SW.0+T.0|olopkt|[POS:B] +klopotr - +aklopt - +akloptr - diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g15.words b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g15.words new file mode 100644 index 000000000..d8e0ba5c5 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g15.words @@ -0,0 +1,18 @@ +lopkotr +loptr +lopkot +lopt +lopktr +lopotr +loptkor +lopkotkor +kaloptr +kalopkotr +loptrko +loptkr +lopkor +olopktr +olopkt +klopotr +aklopt +akloptr diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g15.xml b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g15.xml new file mode 100644 index 000000000..5a2d82fb5 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g15.xml @@ -0,0 +1,40 @@ + + + + LaSwap + ABE + Main +abdegiklmnopqrstuwxyz + + Any + +Main +pk + +ko +PKpk +pc + +ka +PCpc +t + +t +Tt +r + +r +Rr +sw + +SWsw + +T1slotT1 + +lopLOPlop +kamKAMkam +moMOmo + + + + diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g16.expected b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g16.expected new file mode 100644 index 000000000..d097ca0ab --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g16.expected @@ -0,0 +1,18 @@ +lopkotr LOP.0+PK.0+R.0+T.0|lopkotr|[POS:B] +loptr - +lopkot KO.0+LOP.0+T.0|lopkot|[POS:B];LOP.0+PK.0+T.0|lopkot|[POS:B] +lopt LOP.0+T.0|lopt|[POS:B] +lopktr - +lopotr - +loptkor LOP.0+PK.0+R.0+T.0|loptkor|[POS:B] +lopkotkor KO.0+LOP.0+PK.0+R.0+T.0|lopkotkor|[POS:B] +kaloptr LOP.0+PC.0+R.0+T.0|kaloptr|[POS:B] +kalopkotr KO.0+LOP.0+PC.0+R.0+T.0|kalopkotr|[POS:B];LOP.0+PC.0+PK.0+R.0+T.0|kalopkotr|[POS:B] +loptrko - +loptkr - +lopkor LOP.0+PK.0+R.0|lopkor|[POS:B] +lopkomotr LOP.0+MO.0+PK.0+R.0+T.0|lopkomotr|[POS:B] +lopmotr - +lopkotr LOP.0+PK.0+R.0+T.0|lopkotr|[POS:B] +lopkokotr KO.0+LOP.0+PK.0+R.0+T.0|lopkokotr|[POS:B] +lopkomot LOP.0+MO.0+PK.0+T.0|lopkomot|[POS:B] diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g16.words b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g16.words new file mode 100644 index 000000000..2e1af36f0 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g16.words @@ -0,0 +1,18 @@ +lopkotr +loptr +lopkot +lopt +lopktr +lopotr +loptkor +lopkotkor +kaloptr +kalopkotr +loptrko +loptkr +lopkor +lopkomotr +lopmotr +lopkotr +lopkokotr +lopkomot diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g16.xml b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g16.xml new file mode 100644 index 000000000..401853d39 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g16.xml @@ -0,0 +1,42 @@ + + + + LaComp + ABE + Main +abdegiklmnopqrstuwxyz + + Any + +Main +pk + +ko +PKpk +pc + +ka +PCpc +t + +t +Tt +r + +r +Rr +cr + + + + +T1slotT1 + +lopLOPlop +kamKAMkam +moMOmo +koKOko + + + + diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g17.expected b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g17.expected new file mode 100644 index 000000000..28af542b1 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g17.expected @@ -0,0 +1,13 @@ +lopkotr LOP.0+PK.0+R.0+T.0|lopkotr|[POS:B] +loptr - +lopkot LOP.0+PK.0+T.0|lopkot|[POS:B] +lopt LOP.0+T.0|lopt|[POS:B] +lopktr - +lopotr - +loptkor - +lopkotkor - +kaloptr LOP.0+PC.0+R.0+T.0|kaloptr|[POS:B] +kalopkotr LOP.0+PC.0+PK.0+R.0+T.0|kalopkotr|[POS:B] +loptrko - +loptkr - +lopkor LOP.0+PK.0+R.0|lopkor|[POS:B] diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g17.words b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g17.words new file mode 100644 index 000000000..e7afb8792 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g17.words @@ -0,0 +1,13 @@ +lopkotr +loptr +lopkot +lopt +lopktr +lopotr +loptkor +lopkotkor +kaloptr +kalopkotr +loptrko +loptkr +lopkor diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g17.xml b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g17.xml new file mode 100644 index 000000000..e86b46225 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g17.xml @@ -0,0 +1,41 @@ + + + + LaDeep + ABE + Main +abdegiklmnopqrstuwxyz + + Any + +Deep +pk + +ko +PKpk +pc + +ka +PCpc + + +lopLOPlop +kamKAMkam +moMOmo + +Shallow +t + +t +Tt +r + +r +Rr + +T1slotT1 + + + + + diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g18.expected b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g18.expected new file mode 100644 index 000000000..28af542b1 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g18.expected @@ -0,0 +1,13 @@ +lopkotr LOP.0+PK.0+R.0+T.0|lopkotr|[POS:B] +loptr - +lopkot LOP.0+PK.0+T.0|lopkot|[POS:B] +lopt LOP.0+T.0|lopt|[POS:B] +lopktr - +lopotr - +loptkor - +lopkotkor - +kaloptr LOP.0+PC.0+R.0+T.0|kaloptr|[POS:B] +kalopkotr LOP.0+PC.0+PK.0+R.0+T.0|kalopkotr|[POS:B] +loptrko - +loptkr - +lopkor LOP.0+PK.0+R.0|lopkor|[POS:B] diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g18.words b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g18.words new file mode 100644 index 000000000..e7afb8792 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g18.words @@ -0,0 +1,13 @@ +lopkotr +loptr +lopkot +lopt +lopktr +lopotr +loptkor +lopkotkor +kaloptr +kalopkotr +loptrko +loptkr +lopkor diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g18.xml b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g18.xml new file mode 100644 index 000000000..378c1fc5f --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g18.xml @@ -0,0 +1,41 @@ + + + + LaBoth + ABE + Main +abdegiklmnopqrstuwxyz + + Any + +Deep +pk + +ko +PKpk + + +lopLOPlop +kamKAMkam +moMOmo + +Shallow +pc + +ka +PCpc +t + +t +Tt +r + +r +Rr + +T1slotT1 + + + + + diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g19.expected b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g19.expected new file mode 100644 index 000000000..2466498b4 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g19.expected @@ -0,0 +1,16 @@ +lopkotr - +loptr - +lopkot - +lopt LOP.0+T.0|lopt|[POS:B] +lopktr LOP.0+PK.0+R.0+T.0|lopktr|[POS:B] +lopotr - +loptkor LOP.0+PK.0+R.0+T.0|loptkor|[POS:B] +lopkotkor - +kaloptr LOP.0+PC.0+R.0+T.0|kaloptr|[POS:B] +kalopkotr - +loptrko - +loptkr - +lopkor LOP.0+PK.0+R.0|lopkor|[POS:B] +lopktr LOP.0+PK.0+R.0+T.0|lopktr|[POS:B] +lopkt LOP.0+PK.0+T.0|lopkt|[POS:B] +loptr - diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g19.words b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g19.words new file mode 100644 index 000000000..29c6fb45a --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g19.words @@ -0,0 +1,16 @@ +lopkotr +loptr +lopkot +lopt +lopktr +lopotr +loptkor +lopkotkor +kaloptr +kalopkotr +loptrko +loptkr +lopkor +lopktr +lopkt +loptr diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g19.xml b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g19.xml new file mode 100644 index 000000000..3e2d44261 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g19.xml @@ -0,0 +1,42 @@ + + + + LaPrule + ABE + Main +abdegiklmnopqrstuwxyz + + Any +del + + + + + +Main +pk + +ko +PKpk +pc + +ka +PCpc +t + +t +Tt +r + +r +Rr + +T1slotT1 + +lopLOPlop +kamKAMkam +moMOmo + + + + diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g19c.expected b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g19c.expected new file mode 100644 index 000000000..b7ca0a5c0 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g19c.expected @@ -0,0 +1,6 @@ +lipktr LIP.0+PK.0+R.0+T.0|lipktr|[POS:B] +lipkt LIP.0+PK.0+T.0|lipkt|[POS:B] +liptr - +lipkotr - +kaliptr LIP.0+PC.0+R.0+T.0|kaliptr|[POS:B] +liptkor LIP.0+PK.0+R.0+T.0|liptkor|[POS:B] diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g19c.words b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g19c.words new file mode 100644 index 000000000..941e4ef02 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g19c.words @@ -0,0 +1,6 @@ +lipktr +lipkt +liptr +lipkotr +kaliptr +liptkor diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g19c.xml b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g19c.xml new file mode 100644 index 000000000..36efd0195 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g19c.xml @@ -0,0 +1,42 @@ + + + + LaPrule + ABE + Main +abdegiklmnopqrstuwxyz + + Any +del + + + + + +Main +pk + +ko +PKpk +pc + +ka +PCpc +t + +t +Tt +r + +r +Rr + +T1slotT1 + +lipLIPlip +kamKAMkam +moMOmo + + + + diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g20.expected b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g20.expected new file mode 100644 index 000000000..892ff3434 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g20.expected @@ -0,0 +1,17 @@ +lopkotr LOP.0+PK.0+R.0+T.0|lopkotr|[POS:B] +loptr - +lopkot LOP.0+PK.0+T.0|lopkot|[POS:B] +lopt LOP.0+T.0|lopt|[POS:B] +lopktr - +lopotr - +loptkor LOP.0+PK.0+R.0+T.0|loptkor|[POS:B] +lopkotkor - +kaloptr LOP.0+PC.0+R.0+T.0|kaloptr|[POS:B] +kalopkotr LOP.0+PC.0+PK.0+R.0+T.0|kalopkotr|[POS:B] +loptrko - +loptkr - +lopkor LOP.0+PK.0+R.0|lopkor|[POS:B] +lopztr LOP.0+PZ.0+R.0+T.0|lopztr|[POS:B] +lopzt LOP.0+PZ.0+T.0|lopzt|[POS:B] +loptzr LOP.0+PZ.0+R.0+T.0|loptzr|[POS:B] +lopzkotr LOP.0+PK.0+PZ.0+R.0+T.0|lopzkotr|[POS:B] diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g20.words b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g20.words new file mode 100644 index 000000000..149a9c42d --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g20.words @@ -0,0 +1,17 @@ +lopkotr +loptr +lopkot +lopt +lopktr +lopotr +loptkor +lopkotkor +kaloptr +kalopkotr +loptrko +loptkr +lopkor +lopztr +lopzt +loptzr +lopzkotr diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g20.xml b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g20.xml new file mode 100644 index 000000000..b0998562d --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g20.xml @@ -0,0 +1,41 @@ + + + + LaTwo + ABE + Main +abdegiklmnopqrstuwxyz + + Any + +Main +pk + +ko +PKpk +pc + +ka +PCpc +pz + +z +PZpz +t + +t +Tt +r + +r +Rr + +T1slotT1 + +lopLOPlop +kamKAMkam +moMOmo + + + + diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g21.expected b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g21.expected new file mode 100644 index 000000000..b05ec684a --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g21.expected @@ -0,0 +1,18 @@ +lopqtr LOP.0+Q.0+R.0+T.0+Z.0|lopqtr|[POS:B];LOP.0+Q.0+R.0+T.0|lopqtr|[POS:B] +loptr LOP.0+R.0+T.0+Z.0|loptr|[POS:B] +loptqr LOP.0+Q.0+R.0+T.0+Z.0|loptqr|[POS:B];LOP.0+Q.0+R.0+T.0|loptqr|[POS:B] +loptrq LOP.0+Q.0+R.0+T.0+Z.0|loptrq|[POS:B] +lopqtmo LOP.0+MO.0+Q.0+T.0+Z.0|lopqtmo|[POS:B];LOP.0+MO.0+Q.0+T.0|lopqtmo|[POS:B] +loptmo LOP.0+MO.0+T.0+Z.0|loptmo|[POS:B] +lopqtrq - +lopwtrq LOP.0+Q.0+R.0+T.0+W.0+Z.0|lopwtrq|[POS:B];LOP.0+Q.0+R.0+T.0+W.0|lopwtrq|[POS:B] +lopwtrw - +kamdqtr D.0+KAM.0+Q.0+R.0+T.0+Z.0|kamdqtr|[POS:B];D.0+KAM.0+Q.0+R.0+T.0|kamdqtr|[POS:B] +kamqdtr D.0+KAM.0+Q.0+R.0+T.0+Z.0|kamqdtr|[POS:B];D.0+KAM.0+Q.0+R.0+T.0|kamqdtr|[POS:B] +kamdtr D.0+KAM.0+R.0+T.0+Z.0|kamdtr|[POS:B] +kamdtqr D.0+KAM.0+Q.0+R.0+T.0+Z.0|kamdtqr|[POS:B];D.0+KAM.0+Q.0+R.0+T.0|kamdtqr|[POS:B] +lopt LOP.0+T.0+Z.0|lopt|[POS:B];LOP.0+T.0|lopt|[POS:B] +lopqt LOP.0+Q.0+T.0+Z.0|lopqt|[POS:B];LOP.0+Q.0+T.0|lopqt|[POS:B] +loptq LOP.0+Q.0+T.0+Z.0|loptq|[POS:B];LOP.0+Q.0+T.0|loptq|[POS:B] +lopqtrmo LOP.0+MO.0+Q.0+R.0+T.0+Z.0|lopqtrmo|[POS:B];LOP.0+MO.0+Q.0+R.0+T.0|lopqtrmo|[POS:B] +lopzqtmo - diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g21.words b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g21.words new file mode 100644 index 000000000..54e4ea2d5 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g21.words @@ -0,0 +1,18 @@ +lopqtr +loptr +loptqr +loptrq +lopqtmo +loptmo +lopqtrq +lopwtrq +lopwtrw +kamdqtr +kamqdtr +kamdtr +kamdtqr +lopt +lopqt +loptq +lopqtrmo +lopzqtmo diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g21.xml b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g21.xml new file mode 100644 index 000000000..66247204d --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g21.xml @@ -0,0 +1,49 @@ + + + + AdvOblig + ABE + Main +abdegiklmnopqrstuwxyz + + Any + +Main +z + + +Zz +q + +q +Qq +w + +w +Ww +d + +d +Dd +r + +r +Rr +t + +t +Tt +cr + + + + +T1slotT1 + +kamKAMkam +lopLOPlop +moMOmo + + + + diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g6.expected b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g6.expected new file mode 100644 index 000000000..04c4b743b --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g6.expected @@ -0,0 +1,17 @@ +lopqtr LOP.0+Q.0+R.0+T.0|lopqtr|[POS:B] +loptr - +loptqr LOP.0+Q.0+R.0+T.0|loptqr|[POS:B] +loptrq - +lopqtmo LOP.0+MO.0+Q.0+T.0|lopqtmo|[POS:B] +loptmo - +lopqtrq - +lopwtrq LOP.0+Q.0+R.0+T.0+W.0|lopwtrq|[POS:B] +lopwtrw - +kamdqtr D.0+KAM.0+Q.0+R.0+T.0|kamdqtr|[POS:B] +kamqdtr D.0+KAM.0+Q.0+R.0+T.0|kamqdtr|[POS:B] +kamdtr - +kamdtqr D.0+KAM.0+Q.0+R.0+T.0|kamdtqr|[POS:B] +lopt LOP.0+T.0|lopt|[POS:B] +lopqt LOP.0+Q.0+T.0|lopqt|[POS:B] +loptq LOP.0+Q.0+T.0|loptq|[POS:B] +lopqtrmo LOP.0+MO.0+Q.0+R.0+T.0|lopqtrmo|[POS:B] diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g6.words b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g6.words new file mode 100644 index 000000000..e6f62e331 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g6.words @@ -0,0 +1,17 @@ +lopqtr +loptr +loptqr +loptrq +lopqtmo +loptmo +lopqtrq +lopwtrq +lopwtrw +kamdqtr +kamqdtr +kamdtr +kamdtqr +lopt +lopqt +loptq +lopqtrmo diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g6.xml b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g6.xml new file mode 100644 index 000000000..6b6dff2dc --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g6.xml @@ -0,0 +1,45 @@ + + + + AdvOblig + ABE + Main +abdegiklmnopqrstuwxyz + + Any + +Main +q + +q +Qq +w + +w +Ww +d + +d +Dd +r + +r +Rr +t + +t +Tt +cr + + + + +T1slotT1 + +kamKAMkam +lopLOPlop +moMOmo + + + + diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g6b.expected b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g6b.expected new file mode 100644 index 000000000..20a6ff4f0 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g6b.expected @@ -0,0 +1,10 @@ +lopqtr LOP.0+Q.0+R.0+T.0|lopqtr|[POS:B] +loptr - +lopwtr LOP.0+R.0+T.0+W.0|lopwtr|[POS:B] +kamdqtr D.0+KAM.0+Q.0+R.0+T.0|kamdqtr|[POS:B] +kamqdtr D.0+KAM.0+Q.0+R.0+T.0|kamqdtr|[POS:B] +kamdtr - +lopt LOP.0+T.0|lopt|[POS:B] +lopqt LOP.0+Q.0+T.0|lopqt|[POS:B] +lopqtmo - +loptmo - diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g6b.words b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g6b.words new file mode 100644 index 000000000..ffd82196f --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g6b.words @@ -0,0 +1,10 @@ +lopqtr +loptr +lopwtr +kamdqtr +kamqdtr +kamdtr +lopt +lopqt +lopqtmo +loptmo diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g6b.xml b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g6b.xml new file mode 100644 index 000000000..8449259ce --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g6b.xml @@ -0,0 +1,49 @@ + + + + AdvOblig2 + ABE + Main +abdegiklmnopqrstuwxyz + + Any + +Deep +q + +q +Qq +w + +w +Ww +d + +d +Dd + + +kamKAMkam +lopLOPlop +moMOmo + +Shallow +r + +r +Rr +t + +t +Tt +cr + + + + +T1slotT1 + + + + + diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g7.expected b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g7.expected new file mode 100644 index 000000000..206a9b192 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g7.expected @@ -0,0 +1,8 @@ +loptr LOP.0+R.0+T.0|loptr|[POS:B] +lopt LOP.0+T.0|lopt|[POS:B] +lopqtr LOP.0+Q.0+R.0+T.0|lopqtr|[POS:B] +loptqr LOP.0+Q.0+R.0+T.0|loptqr|[POS:B] +aloptr LOP.0+P.0+R.0+T.0|aloptr|[POS:B] +alopt LOP.0+P.0+T.0|alopt|[POS:B] +lopr LOP.0+R.0|lopr|[POS:B] +alopqtr LOP.0+P.0+Q.0+R.0+T.0|alopqtr|[POS:B] diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g7.words b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g7.words new file mode 100644 index 000000000..b7dfd3466 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g7.words @@ -0,0 +1,8 @@ +loptr +lopt +lopqtr +loptqr +aloptr +alopt +lopr +alopqtr diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g7.xml b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g7.xml new file mode 100644 index 000000000..63ab4ab1c --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g7.xml @@ -0,0 +1,38 @@ + + + + AdvShared + AB + Main +abdegiklmnopqrstuwxyz + + Any + +Main +q + +q +Qq +r + +r +Rr +t + +t +Tt +p + +a +Pp + +T1slotT1 +T2slotT2 +T3slotT3 + +kamKAMkam +lopLOPlop + + + + diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g8.expected b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g8.expected new file mode 100644 index 000000000..5db241d06 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g8.expected @@ -0,0 +1,24 @@ +kamd - +kamt KAMB.0+T.0|kamt|[POS:B] +kamdt D.0+KAM.0+T.0|kamdt|[POS:B] +kamet E.0+KAM.0+T.0|kamet|[POS:B];E.0+KAMB.0+T.0|kamet|[POS:B] +kamu KAMB.0+U.0|kamu|[POS:B] +kamut KAMB.0+T.0+U.0|kamut|[POS:B] +lopt LOP.0+T.0|lopt|[POS:B] +lopu LOP.0+U.0|lopu|[POS:B] +nukx NUK.0+X.0|nukx|[POS:C] +kamx KAM.0+X.0|kamx|[POS:A] +lopx - +akamt KAMB.0+P.0+T.0|akamt|[POS:B] +akamdt D.0+KAM.0+P.0+T.0|akamdt|[POS:B] +alopt LOP.0+P.0+T.0|alopt|[POS:B] +kammo - +lopmo - +kamdmo - +pat - +pau PA.0+U.0|pau|ANY +pad D.0+PA.0|pad|[POS:B] +nukqx NUK.0+Q.0+X.0|nukqx|[POS:C] +nukxq NUK.0+Q.0+X.0|nukxq|[POS:C] +kamtd - +nuket E.0+NUK.0+T.0|nuket|[POS:B] diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g8.words b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g8.words new file mode 100644 index 000000000..c1c4dd165 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g8.words @@ -0,0 +1,24 @@ +kamd +kamt +kamdt +kamet +kamu +kamut +lopt +lopu +nukx +kamx +lopx +akamt +akamdt +alopt +kammo +lopmo +kamdmo +pat +pau +pad +nukqx +nukxq +kamtd +nuket diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g8.xml b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g8.xml new file mode 100644 index 000000000..ff3b32366 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/g8.xml @@ -0,0 +1,59 @@ + + + + AdvGate + ABCE + Main +abdegiklmnopqrstuwxyz + + Any + +Main +d + +d +Dd +e + +e +Ee +t + +t +Tt +u + +u +Uu +x + +x +Xx +p + +a +Pp +cr + + + +q + +q +Qq + +T1slotT1 +T2slotT2 +T3slotT3 +T4slotT4 + +kamKAMkam +kamKAMBkamb +lopLOPlop +nukNUKnuk +moMOmo +paPApa + + + + From 0dfee20abd5e6c3313c9b6ef9a75c3f7c105bbbb Mon Sep 17 00:00:00 2001 From: John Lambert Date: Thu, 12 Nov 2026 16:46:38 -0500 Subject: [PATCH 3/8] Shorten two comments to the comment-hygiene budget Co-Authored-By: Claude Opus 5.5 --- .../FinalTemplatePartialReachability.cs | 2 +- .../PartialRulePruneTests.cs | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/SIL.Machine.Morphology.HermitCrab/FinalTemplatePartialReachability.cs b/src/SIL.Machine.Morphology.HermitCrab/FinalTemplatePartialReachability.cs index 082ec09b2..6a7cd3ee0 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/FinalTemplatePartialReachability.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/FinalTemplatePartialReachability.cs @@ -118,7 +118,7 @@ FeatureStruct output return input.Where(req.Contains); } - /// The part-of-speech values a feature structure allows, or null if it does not constrain it. + /// The part-of-speech values a feature structure allows, or null if it leaves them open. private static HashSet Constraint(HashSet all, FeatureStruct fs) { if (fs == null) diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/PartialRulePruneTests.cs b/tests/SIL.Machine.Morphology.HermitCrab.Tests/PartialRulePruneTests.cs index 7c381edfb..c9eace0cd 100644 --- a/tests/SIL.Machine.Morphology.HermitCrab.Tests/PartialRulePruneTests.cs +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/PartialRulePruneTests.cs @@ -1,11 +1,10 @@ -using NUnit.Framework; +using NUnit.Framework; using SIL.Machine.Morphology.HermitCrab.MorphologicalRules; namespace SIL.Machine.Morphology.HermitCrab; -// The grammars in TestData/PartialPrune put partial material where a careless prune of final templates would miss -// it. The .expected files hold the analyses (allomorphs, shape, syntactic features) found before -// the prune was narrowed, so a lost, extra or changed analysis fails a test. +// Each grammar in TestData/PartialPrune puts partial material where a careless prune would miss it. The .expected +// files hold master's analyses, so a lost, extra or changed one fails. public class PartialRulePruneTests { private static readonly string[] Grammars = @@ -108,8 +107,8 @@ public void ParseWord_PartialRuleGrammar_KeepsEveryAnalysis(string grammar, int Assert.That(differences, Is.Empty); } - // Rule-selector calls are the work counter: every analysis and synthesis rule asks once per application. With - // one thread the count is deterministic, so a bound separates a prune that fires from one that does not. + // Rule-selector calls count work. At one thread the count is deterministic, so a bound tells a prune that + // fires from one that does not. [TestCase("g8", "akamdt", 130, TestName = "TemplateNoPartialWordCanReach")] [TestCase("g6", "loptr", 18, TestName = "ShapeHasNoRoomForPartialMaterialContiguous")] [TestCase("g12", "loptr", 16, TestName = "ShapeHasNoRoomForPartialMaterialExistence")] @@ -129,8 +128,8 @@ public void ParseWord_PartialRuleGrammar_PrunesWhatNoPartialWordCanUse(string gr Assert.That(calls, Is.LessThanOrEqualTo(maxCalls)); } - // The look-ahead assumes that unapplying a rule copies nodes of its input unchanged and adds only nodes it - // inserts or rebuilds. A new kind of output action has to be considered there before it is listed here. + // The look-ahead assumes unapplying a rule copies input nodes and adds only inserted or rebuilt ones. Weigh a + // new output action type there before listing it here. [Test] public void MorphologicalOutputAction_Subtypes_AreTheOnesPartialRuleLookAheadKnows() { From 8d06d06030b9006efcfba8d614def54a58775e16 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Thu, 12 Nov 2026 17:28:54 -0500 Subject: [PATCH 4/8] Treat a compound's non-head as material between head parts in the partial-rule look-ahead, and test the shapes the look-ahead reads A compounding subrule whose output was head, non-head, head was classed as a contiguous run of the input, so the prune dropped a parse master finds (cx1b). A non-head copy after a head copy now counts as an insertion between head copies. New grammars cx1b and cx4 to cx8 cover that compound, ModifyFromInput, InsertSimpleContext, an infix with a copied part, a non-head-first compound and a head part copied twice; cx2 makes the flagged and unflagged word differ only in NeedsPartial so removing it from Word equality or the merge key fails; cx3 pins that a lexical family in a partial-rule grammar returns each analysis once where master returns it twice. Co-Authored-By: Claude Opus 5.5 --- .../PartialRuleLookAhead.cs | 8 +++- .../PartialRulePruneTests.cs | 23 ++++++++++- .../TestData/PartialPrune/cx1b.expected | 6 +++ .../TestData/PartialPrune/cx1b.words | 6 +++ .../TestData/PartialPrune/cx1b.xml | 38 ++++++++++++++++++ .../TestData/PartialPrune/cx2.expected | 6 +++ .../TestData/PartialPrune/cx2.words | 6 +++ .../TestData/PartialPrune/cx2.xml | 39 +++++++++++++++++++ .../TestData/PartialPrune/cx3.expected | 7 ++++ .../TestData/PartialPrune/cx3.words | 7 ++++ .../TestData/PartialPrune/cx3.xml | 38 ++++++++++++++++++ .../TestData/PartialPrune/cx4.expected | 7 ++++ .../TestData/PartialPrune/cx4.words | 7 ++++ .../TestData/PartialPrune/cx4.xml | 37 ++++++++++++++++++ .../TestData/PartialPrune/cx5.expected | 6 +++ .../TestData/PartialPrune/cx5.words | 6 +++ .../TestData/PartialPrune/cx5.xml | 36 +++++++++++++++++ .../TestData/PartialPrune/cx6.expected | 6 +++ .../TestData/PartialPrune/cx6.words | 6 +++ .../TestData/PartialPrune/cx6.xml | 36 +++++++++++++++++ .../TestData/PartialPrune/cx7.expected | 6 +++ .../TestData/PartialPrune/cx7.words | 6 +++ .../TestData/PartialPrune/cx7.xml | 36 +++++++++++++++++ .../TestData/PartialPrune/cx8.expected | 6 +++ .../TestData/PartialPrune/cx8.words | 6 +++ .../TestData/PartialPrune/cx8.xml | 36 +++++++++++++++++ 26 files changed, 425 insertions(+), 2 deletions(-) create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx1b.expected create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx1b.words create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx1b.xml create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx2.expected create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx2.words create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx2.xml create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx3.expected create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx3.words create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx3.xml create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx4.expected create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx4.words create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx4.xml create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx5.expected create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx5.words create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx5.xml create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx6.expected create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx6.words create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx6.xml create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx7.expected create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx7.words create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx7.xml create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx8.expected create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx8.words create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx8.xml diff --git a/src/SIL.Machine.Morphology.HermitCrab/PartialRuleLookAhead.cs b/src/SIL.Machine.Morphology.HermitCrab/PartialRuleLookAhead.cs index 99852f1b3..e57c9f0aa 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/PartialRuleLookAhead.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/PartialRuleLookAhead.cs @@ -189,7 +189,13 @@ private static bool Inspect(Transform t, List rebuilt, out bool c { int index = names.IndexOf(action.PartName); if (index < 0) - continue; // a non-head part of a compound + { + // A non-head part leaves the head shape, so it separates head parts that follow it from + // those before it just as an inserted segment does. + if (copies.Count > 0) + insertedAfterCopy = true; + continue; + } if (insertedAfterCopy) insertedBetween = true; copies.Add(index); diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/PartialRulePruneTests.cs b/tests/SIL.Machine.Morphology.HermitCrab.Tests/PartialRulePruneTests.cs index c9eace0cd..12caaa85d 100644 --- a/tests/SIL.Machine.Morphology.HermitCrab.Tests/PartialRulePruneTests.cs +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/PartialRulePruneTests.cs @@ -28,6 +28,14 @@ public class PartialRulePruneTests "g19c", // phonological deletion hides a partial segment, minimal "g20", // three partial rules "g21", // compounding and a partial rule the look-ahead cannot reason about + "cx1b", // a compound puts its non-head between two head parts, hiding the partial material + "cx2", // a flagged and an unflagged word differ only in NeedsPartial and must both survive + "cx3", // lexical family and a partial rule: master returns duplicates, see the pinning test below + "cx4", // ModifyFromInput changes the partial material + "cx5", // InsertSimpleContext splits the partial material + "cx6", // an infix and a copy of a part (reduplication) rearrange the partial material + "cx7", // a compound with the non-head before the head parts + "cx8", // a compound copies one head part twice around the non-head ]; // Grammars whose words need the deletion rules to be reapplied. @@ -107,6 +115,18 @@ public void ParseWord_PartialRuleGrammar_KeepsEveryAnalysis(string grammar, int Assert.That(differences, Is.Empty); } + // In a grammar with partial rules and a lexical family, master returns each of these words twice and the + // prune returns them once: a family member that replaces the root is a fresh word that forgets the pending + // final template. Only duplicates are lost, never a distinct analysis (cx3.expected holds the distinct ones). + // This pins the behaviour; if it changes, update the doc comment on Morpher.IsFinalTemplatePrunable. + [TestCase("kamdtr")] + [TestCase("kamdtrt")] + public void ParseWord_FamilyAndPartialRuleGrammar_ReturnsEachAnalysisOnce(string word) + { + Morpher morpher = CreateMorpher("cx3", maxDegreeOfParallelism: 1); + Assert.That(morpher.ParseWord(word).Count(), Is.EqualTo(1)); + } + // Rule-selector calls count work. At one thread the count is deterministic, so a bound tells a prune that // fires from one that does not. [TestCase("g8", "akamdt", 130, TestName = "TemplateNoPartialWordCanReach")] @@ -129,7 +149,8 @@ public void ParseWord_PartialRuleGrammar_PrunesWhatNoPartialWordCanUse(string gr } // The look-ahead assumes unapplying a rule copies input nodes and adds only inserted or rebuilt ones. Weigh a - // new output action type there before listing it here. + // new output action type there before listing it here. This guards the action types; the cx grammars above + // guard how the look-ahead reads each rule shape (compound non-head, modify, simple context, copies). [Test] public void MorphologicalOutputAction_Subtypes_AreTheOnesPartialRuleLookAheadKnows() { diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx1b.expected b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx1b.expected new file mode 100644 index 000000000..ba3061378 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx1b.expected @@ -0,0 +1,6 @@ +lopkmootr LOP.0+MO.0+PK.0+R.0+T.0|lopkmootr|[POS:B] +lopkmoot LOP.0+MO.0+PK.0+T.0|lopkmoot|[POS:B] +lopkootr - +lopkmoo LOP.0+MO.0+PK.0|lopkmoo|[POS:B] +lopkotr LOP.0+PK.0+R.0+T.0|lopkotr|[POS:B] +lopkmootrko - diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx1b.words b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx1b.words new file mode 100644 index 000000000..7e7021f3e --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx1b.words @@ -0,0 +1,6 @@ +lopkmootr +lopkmoot +lopkootr +lopkmoo +lopkotr +lopkmootrko diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx1b.xml b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx1b.xml new file mode 100644 index 000000000..f1b64b0ea --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx1b.xml @@ -0,0 +1,38 @@ + + + + LaComp + ABE + Main +abdegiklmnopqrstuwxyz + + AnyKO + +Main +pk + +ko +PKpk +t + +t +Tt +r + +r +Rr +cr + + + + +T1slotT1 + +lopLOPlop +kamKAMkam +moMOmo +koKOko + + + + diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx2.expected b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx2.expected new file mode 100644 index 000000000..fefc8c39a --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx2.expected @@ -0,0 +1,6 @@ +qoptr QOP.0+R.0+T.0|qoptr|[POS:B] +qopt QOP.0+T.0|qopt|[POS:B] +loptr LOP.0+R.0+T.0|loptr|[POS:B] +lopqtr LOP.0+Q.0+R.0+T.0|lopqtr|[POS:B] +qopqtr Q.0+QOP.0+R.0+T.0|qopqtr|[POS:B] +aqoptr P.0+QOP.0+R.0+T.0|aqoptr|[POS:B] diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx2.words b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx2.words new file mode 100644 index 000000000..23cdfd4aa --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx2.words @@ -0,0 +1,6 @@ +qoptr +qopt +loptr +lopqtr +qopqtr +aqoptr diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx2.xml b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx2.xml new file mode 100644 index 000000000..966fad77f --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx2.xml @@ -0,0 +1,39 @@ + + + + AdvShared + AB + Main +abdegiklmnopqrstuwxyz + + Any + +Main +q + +q +Qq +r + +r +Rr +t + +t +Tt +p + +a +Pp + +T1slotT1 +T2slotT2 +T3slotT3 + +qopQOPqop +kamKAMkam +lopLOPlop + + + + diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx3.expected b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx3.expected new file mode 100644 index 000000000..abbe3c63f --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx3.expected @@ -0,0 +1,7 @@ +kamdtr KAMDTR.0|kamdtr|[POS:A] +kamdt - +kamd - +kamtr - +kamdtrt KAMDTR.0+RR.0|kamdtrt|[Head:[hf:x], POS:A] +kamzdtr - +kamdtrz - diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx3.words b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx3.words new file mode 100644 index 000000000..20b67b8b1 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx3.words @@ -0,0 +1,7 @@ +kamdtr +kamdt +kamd +kamtr +kamdtrt +kamzdtr +kamdtrz diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx3.xml b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx3.xml new file mode 100644 index 000000000..07c92194d --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx3.xml @@ -0,0 +1,38 @@ + + + + AdvBlock + A + hfxy + Main +abdegiklmnopqrstuwxyz + + Any + fam1 + +Main +d1 + +d +D1d1 +r1 + +r +R1r1 +pz + +z +PZpz +rr + +t +RRrr + +T1slotT1 + +kamKAMkam +kamdtrKAMDTRkamdtr + + + + diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx4.expected b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx4.expected new file mode 100644 index 000000000..9466c760c --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx4.expected @@ -0,0 +1,7 @@ +lopko LOP.0+PK.0|lopko|[POS:B] +lopkot LOP.0+PK.0+T.0|lopkot|[POS:B] +lopku LOP.0+PK.0+X.0|lopku|[POS:B] +lopkut LOP.0+PK.0+T.0+X.0|lopkut|[POS:B] +lopkutr LOP.0+PK.0+R.0+T.0+X.0|lopkutr|[POS:B] +lopkotr LOP.0+PK.0+R.0+T.0|lopkotr|[POS:B] +lopkuot - diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx4.words b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx4.words new file mode 100644 index 000000000..11566ff5f --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx4.words @@ -0,0 +1,7 @@ +lopko +lopkot +lopku +lopkut +lopkutr +lopkotr +lopkuot diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx4.xml b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx4.xml new file mode 100644 index 000000000..edf22b171 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx4.xml @@ -0,0 +1,37 @@ + + + + LaComp + ABE + segabdegiklmnopqrstuwxyz + Main +abdegiklmnopqrstuwxyz + + AnyKUO + +Main +pk + +ko +PKpk +t + +t +Tt +r + +r +Rr +xXx + + +T1slotT1 + +lopLOPlop +kamKAMkam +moMOmo +koKOko + + + + diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx5.expected b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx5.expected new file mode 100644 index 000000000..114bbe686 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx5.expected @@ -0,0 +1,6 @@ +lopkot LOP.0+PK.0+T.0|lopkot|[POS:B] +lopkuo LOP.0+PK.0+X.0|lopkuo|[POS:B] +lopkuot LOP.0+PK.0+T.0+X.0|lopkuot|[POS:B] +lopkuotr LOP.0+PK.0+R.0+T.0+X.0|lopkuotr|[POS:B] +lopkuor LOP.0+PK.0+R.0+X.0|lopkuor|[POS:B] +lopkotr LOP.0+PK.0+R.0+T.0|lopkotr|[POS:B] diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx5.words b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx5.words new file mode 100644 index 000000000..51bfdc97a --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx5.words @@ -0,0 +1,6 @@ +lopkot +lopkuo +lopkuot +lopkuotr +lopkuor +lopkotr diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx5.xml b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx5.xml new file mode 100644 index 000000000..1d2c2c870 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx5.xml @@ -0,0 +1,36 @@ + + + + LaComp + ABE + Main +abdegiklmnopqrstuwxyz + + AnyKUO + +Main +pk + +ko +PKpk +t + +t +Tt +r + +r +Rr +xXx + + +T1slotT1 + +lopLOPlop +kamKAMkam +moMOmo +koKOko + + + + diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx6.expected b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx6.expected new file mode 100644 index 000000000..4cb2e05be --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx6.expected @@ -0,0 +1,6 @@ +lopkot LOP.0+PK.0+T.0|lopkot|[POS:B] +lopkmolopk LOP.0+PK.0+X.0|lopkmolopk|[POS:B] +lopkmolopkt LOP.0+PK.0+T.0+X.0|lopkmolopkt|[POS:B] +lopkmolopktr LOP.0+PK.0+R.0+T.0+X.0|lopkmolopktr|[POS:B] +lopkmolopr - +lopkotr LOP.0+PK.0+R.0+T.0|lopkotr|[POS:B] diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx6.words b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx6.words new file mode 100644 index 000000000..96e3e9360 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx6.words @@ -0,0 +1,6 @@ +lopkot +lopkmolopk +lopkmolopkt +lopkmolopktr +lopkmolopr +lopkotr diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx6.xml b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx6.xml new file mode 100644 index 000000000..9a802b20e --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx6.xml @@ -0,0 +1,36 @@ + + + + LaComp + ABE + Main +abdegiklmnopqrstuwxyz + + AnyKUO + +Main +pk + +ko +PKpk +t + +t +Tt +r + +r +Rr +xmXx + + +T1slotT1 + +lopLOPlop +kamKAMkam +moMOmo +koKOko + + + + diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx7.expected b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx7.expected new file mode 100644 index 000000000..937f46904 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx7.expected @@ -0,0 +1,6 @@ +lopkot LOP.0+PK.0+T.0|lopkot|[POS:B] +molopko LOP.0+MO.0+PK.0|molopko|[POS:B] +molopkot LOP.0+MO.0+PK.0+T.0|molopkot|[POS:B] +molopkotr LOP.0+MO.0+PK.0+R.0+T.0|molopkotr|[POS:B] +molopkt - +lopkotr LOP.0+PK.0+R.0+T.0|lopkotr|[POS:B] diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx7.words b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx7.words new file mode 100644 index 000000000..19951529c --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx7.words @@ -0,0 +1,6 @@ +lopkot +molopko +molopkot +molopkotr +molopkt +lopkotr diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx7.xml b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx7.xml new file mode 100644 index 000000000..4ec4d70b6 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx7.xml @@ -0,0 +1,36 @@ + + + + LaComp + ABE + Main +abdegiklmnopqrstuwxyz + + AnyKUO + +Main +pk + +ko +PKpk +t + +t +Tt +r + +r +Rr +cr + + +T1slotT1 + +lopLOPlop +kamKAMkam +moMOmo +koKOko + + + + diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx8.expected b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx8.expected new file mode 100644 index 000000000..95fb082d7 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx8.expected @@ -0,0 +1,6 @@ +lopkot LOP.0+PK.0+T.0|lopkot|[POS:B] +lopkmolopko LOP.0+MO.0+PK.0|lopkmolopko|[POS:B] +lopkmolopkot LOP.0+MO.0+PK.0+T.0|lopkmolopkot|[POS:B] +lopkmolopkotr LOP.0+MO.0+PK.0+R.0+T.0|lopkmolopkotr|[POS:B] +lopkmolopkt - +lopkotr LOP.0+PK.0+R.0+T.0|lopkotr|[POS:B] diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx8.words b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx8.words new file mode 100644 index 000000000..3e058dd1a --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx8.words @@ -0,0 +1,6 @@ +lopkot +lopkmolopko +lopkmolopkot +lopkmolopkotr +lopkmolopkt +lopkotr diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx8.xml b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx8.xml new file mode 100644 index 000000000..4f9f171ee --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx8.xml @@ -0,0 +1,36 @@ + + + + LaComp + ABE + Main +abdegiklmnopqrstuwxyz + + AnyKUO + +Main +pk + +ko +PKpk +t + +t +Tt +r + +r +Rr +cr + + +T1slotT1 + +lopLOPlop +kamKAMkam +moMOmo +koKOko + + + + From 9934032fd98f07469dae93813061f387fcf17b57 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Thu, 12 Nov 2026 17:28:55 -0500 Subject: [PATCH 5/8] Skip flagged analyses in root guessing and correct the lexical-family caveat and merge-key doc Root guessing now skips analyses that owe a partial rule, as lexical lookup does, since a guessed root is never partial. The IsFinalTemplatePrunable comment says that in a grammar with both partial rules and lexical families only a duplicate analysis can be lost, which master returns and the prune does not. docs/review/hermitcrab.md lists NeedsPartial among the merge-key fields. Co-Authored-By: Claude Opus 5.5 --- docs/review/hermitcrab.md | 2 +- src/SIL.Machine.Morphology.HermitCrab/Morpher.cs | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/review/hermitcrab.md b/docs/review/hermitcrab.md index 6bc033c7e..49ffb4bcf 100644 --- a/docs/review/hermitcrab.md +++ b/docs/review/hermitcrab.md @@ -11,7 +11,7 @@ Governs `src/SIL.Machine.Morphology.HermitCrab/**/*.cs`. whether equivalent analyses can be merged safely. The merge key (`AnalysisMergeKey`) compares exactly these fields: shape, stratum identity, syntactic feature structure, realizational feature structure, non-head count, per-rule - unapplication counts, and `FinalTemplateState`. Check a new field against that list. + unapplication counts, `FinalTemplateState`, and `NeedsPartial`. Check a new field against that list. - Inspect allocations and retained object lifetimes only in changed inner loops. If the change claims a performance improvement, require a reproducible benchmark or measured artifact in addition to semantic regression tests. diff --git a/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs b/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs index de0335c08..868310892 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs @@ -95,10 +95,10 @@ public Morpher(ITraceManager traceManager, Language lang, int maxDegreeOfParalle /// derivation this refuses has a non-partial rule after the final template, which is allowed only when the /// word is partial there: a partial rule applied before the template. A partial rule leaves the analysis /// state at none, so it never arms this. Templates no partial word can reach are pruned outright, and - /// others when the shape shows that no partial rule can still be unapplied. As with the prune it narrows, - /// a lexical family can make this drop a duplicate analysis: a family member that replaces the root is a - /// fresh word that forgets the pending final template. FieldWorks never writes a family; no distinct - /// analysis was lost in a grammar that has one. + /// others when the shape shows that no partial rule can still be unapplied. In a grammar with both partial + /// rules and lexical families this can drop a duplicate analysis that master returns: a family member + /// that replaces the root is a fresh word that forgets the pending final template. Only a duplicate is + /// lost, never a distinct analysis. FieldWorks never writes a family. /// internal bool IsFinalTemplatePrunable(AffixTemplate template, Word input) { @@ -249,7 +249,8 @@ public IEnumerable ParseWord(string word, out object trace, bool guessRoot { // Guess roots when there are no results. List matches = new List(); - foreach (Word analysisWord in origAnalyses) + // A guessed root is never partial, so it cannot pay the partial rule a flagged analysis owes. + foreach (Word analysisWord in origAnalyses.Where(w => !w.NeedsPartial)) { var lexicalGuesses = LexicalGuess(analysisWord).Distinct(); foreach (Word synthesisWord in lexicalGuesses) From 71ce0fe0b94cbd2698e73e9f93c34177ad506dc0 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Thu, 12 Nov 2026 17:57:08 -0500 Subject: [PATCH 6/8] Pin the ModifyFromInput switch-off with a test and give the root-guessing skip its real reason Grammar cx9b modifies an unbounded part, which analysis widens without rebuilding it, so a look-ahead that reads ModifyFromInput as a copy loses lopkutr; the test fails with the switch-off removed. The root-guessing comment now says a partial root takes no template, so the slot rule a flagged analysis owes is never applied. Co-Authored-By: Claude Opus 5.5 --- .../Morpher.cs | 2 +- .../PartialRulePruneTests.cs | 1 + .../TestData/PartialPrune/cx9b.expected | 9 +++++ .../TestData/PartialPrune/cx9b.words | 9 +++++ .../TestData/PartialPrune/cx9b.xml | 37 +++++++++++++++++++ 5 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx9b.expected create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx9b.words create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx9b.xml diff --git a/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs b/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs index 868310892..85482a88e 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs @@ -249,7 +249,7 @@ public IEnumerable ParseWord(string word, out object trace, bool guessRoot { // Guess roots when there are no results. List matches = new List(); - // A guessed root is never partial, so it cannot pay the partial rule a flagged analysis owes. + // A partial root takes no template, so the slot rule a flagged analysis owes is never applied. foreach (Word analysisWord in origAnalyses.Where(w => !w.NeedsPartial)) { var lexicalGuesses = LexicalGuess(analysisWord).Distinct(); diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/PartialRulePruneTests.cs b/tests/SIL.Machine.Morphology.HermitCrab.Tests/PartialRulePruneTests.cs index 12caaa85d..fe416b8bd 100644 --- a/tests/SIL.Machine.Morphology.HermitCrab.Tests/PartialRulePruneTests.cs +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/PartialRulePruneTests.cs @@ -32,6 +32,7 @@ public class PartialRulePruneTests "cx2", // a flagged and an unflagged word differ only in NeedsPartial and must both survive "cx3", // lexical family and a partial rule: master returns duplicates, see the pinning test below "cx4", // ModifyFromInput changes the partial material + "cx9b", // ModifyFromInput on an unbounded part widens the node the partial rule needs "cx5", // InsertSimpleContext splits the partial material "cx6", // an infix and a copy of a part (reduplication) rearrange the partial material "cx7", // a compound with the non-head before the head parts diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx9b.expected b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx9b.expected new file mode 100644 index 000000000..45dbfdde5 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx9b.expected @@ -0,0 +1,9 @@ +lopko LOP.0+PK.0|lopko|[POS:B] +lopkot LOP.0+PK.0+T.0|lopkot|[POS:B] +lopku LOP.0+PK.0+X.0|lopku|[POS:B] +lopkut LOP.0+PK.0+T.0+X.0|lopkut|[POS:B] +lopkutr LOP.0+PK.0+R.0+T.0+X.0|lopkutr|[POS:B] +lopkotr LOP.0+PK.0+R.0+T.0|lopkotr|[POS:B] +lopkuot - +lopkutr LOP.0+PK.0+R.0+T.0+X.0|lopkutr|[POS:B] +lopkuutr - diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx9b.words b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx9b.words new file mode 100644 index 000000000..4256efb37 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx9b.words @@ -0,0 +1,9 @@ +lopko +lopkot +lopku +lopkut +lopkutr +lopkotr +lopkuot +lopkutr +lopkuutr diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx9b.xml b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx9b.xml new file mode 100644 index 000000000..3c8b3883a --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx9b.xml @@ -0,0 +1,37 @@ + + + + LaComp + ABE + segabdegiklmnopqrstuwxyz + Main +abdegiklmnopqrstuwxyz + + AnyKUO + +Main +pk + +ko +PKpk +t + +t +Tt +r + +r +Rr +xXx + + +T1slotT1 + +lopLOPlop +kamKAMkam +moMOmo +koKOko + + + + From f6c0b8411a95ecef454079c8a9da706fb14ab7e7 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Thu, 12 Nov 2026 18:04:36 -0500 Subject: [PATCH 7/8] Shorten two test comments to the comment-hygiene budget Co-Authored-By: Claude Opus 5.5 --- .../PartialRulePruneTests.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/PartialRulePruneTests.cs b/tests/SIL.Machine.Morphology.HermitCrab.Tests/PartialRulePruneTests.cs index fe416b8bd..8a41c3dc1 100644 --- a/tests/SIL.Machine.Morphology.HermitCrab.Tests/PartialRulePruneTests.cs +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/PartialRulePruneTests.cs @@ -116,10 +116,7 @@ public void ParseWord_PartialRuleGrammar_KeepsEveryAnalysis(string grammar, int Assert.That(differences, Is.Empty); } - // In a grammar with partial rules and a lexical family, master returns each of these words twice and the - // prune returns them once: a family member that replaces the root is a fresh word that forgets the pending - // final template. Only duplicates are lost, never a distinct analysis (cx3.expected holds the distinct ones). - // This pins the behaviour; if it changes, update the doc comment on Morpher.IsFinalTemplatePrunable. + // Master returns each word twice in a family grammar with partial rules; the prune drops only duplicates. [TestCase("kamdtr")] [TestCase("kamdtrt")] public void ParseWord_FamilyAndPartialRuleGrammar_ReturnsEachAnalysisOnce(string word) @@ -150,8 +147,7 @@ public void ParseWord_PartialRuleGrammar_PrunesWhatNoPartialWordCanUse(string gr } // The look-ahead assumes unapplying a rule copies input nodes and adds only inserted or rebuilt ones. Weigh a - // new output action type there before listing it here. This guards the action types; the cx grammars above - // guard how the look-ahead reads each rule shape (compound non-head, modify, simple context, copies). + // new output action type there before listing it here. [Test] public void MorphologicalOutputAction_Subtypes_AreTheOnesPartialRuleLookAheadKnows() { From 2db0929b1515ad6faf46dd61af46509f2f77e109 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Thu, 12 Nov 2026 20:20:27 -0500 Subject: [PATCH 8/8] Trace every prune of an owing analysis, track state only with a final template, and ignore slot and clitic partial rules in reachability An analysis dropped because it still owes a partial rule now leaves a TemplateAnalysisOutput trace node with FailureReason.OwedPartialCannotUnapply, the owing final template as source and the grammar's partial rules as FailureObject, as UnreachableRequiredSlot does in #575 (EndUnapplyTemplate gains the failureObj parameter, Trace gains FailureObject). Final-template state and the look-ahead are enabled only when the grammar has a final template that can be pruned. Reachability seeds only from non-template, non-clitic partial rules, the ones synthesis marks partial, so a grammar whose only partial rules sit in slots or are clitics prunes like one without partial rules. New grammars cx10 to cx12 and tests for the trace, the tracking flag and the work bound. Co-Authored-By: Claude Opus 5.5 --- .../AnalysisAffixTemplateRule.cs | 16 ++-- .../AnalysisStratumRule.cs | 6 ++ .../FinalTemplatePartialReachability.cs | 11 ++- .../ITraceManager.cs | 9 ++- .../Morpher.cs | 44 ++++++++++- .../PartialRuleLookAhead.cs | 2 +- .../Trace.cs | 7 ++ .../TraceManager.cs | 17 +++- src/SIL.Machine.Morphology.HermitCrab/Word.cs | 13 ++++ .../PartialRulePruneTests.cs | 77 ++++++++++++++++++- .../TestData/PartialPrune/cx10.expected | 10 +++ .../TestData/PartialPrune/cx10.words | 10 +++ .../TestData/PartialPrune/cx10.xml | 38 +++++++++ .../TestData/PartialPrune/cx11.expected | 10 +++ .../TestData/PartialPrune/cx11.words | 10 +++ .../TestData/PartialPrune/cx11.xml | 39 ++++++++++ .../TestData/PartialPrune/cx12.expected | 10 +++ .../TestData/PartialPrune/cx12.words | 10 +++ .../TestData/PartialPrune/cx12.xml | 38 +++++++++ 19 files changed, 359 insertions(+), 18 deletions(-) create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx10.expected create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx10.words create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx10.xml create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx11.expected create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx11.words create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx11.xml create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx12.expected create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx12.words create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx12.xml diff --git a/src/SIL.Machine.Morphology.HermitCrab/AnalysisAffixTemplateRule.cs b/src/SIL.Machine.Morphology.HermitCrab/AnalysisAffixTemplateRule.cs index e4dcb160f..27053349c 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/AnalysisAffixTemplateRule.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/AnalysisAffixTemplateRule.cs @@ -54,7 +54,8 @@ public IEnumerable Apply(Word input) _template, input, false, - FailureReason.NonPartialRuleProhibitedAfterFinalTemplate + FailureReason.NonPartialRuleProhibitedAfterFinalTemplate, + null ); } return Enumerable.Empty(); @@ -63,7 +64,10 @@ public IEnumerable Apply(Word input) Word inWord = input.Clone(); // The prune check above passed, so the word is partial at this template or the analysis fails. if (afterNonTemplate) + { inWord.NeedsPartial = true; + inWord.OwedPartialTemplate = _template; + } inWord.Freeze(); var output = new HashSet(FreezableEqualityComparer.Default); @@ -93,13 +97,13 @@ private void ApplySlots(Word inWord, int index, HashSet output) if (!_template.Slots[i].Optional) { if (_morpher.TraceManager.IsTracing) - _morpher.TraceManager.EndUnapplyTemplate(_template, inWord, false, FailureReason.None); + _morpher.TraceManager.EndUnapplyTemplate(_template, inWord, false, FailureReason.None, null); return; } } if (_morpher.TraceManager.IsTracing) - _morpher.TraceManager.EndUnapplyTemplate(_template, inWord, true, FailureReason.None); + _morpher.TraceManager.EndUnapplyTemplate(_template, inWord, true, FailureReason.None, null); output.Add(inWord); } @@ -135,7 +139,8 @@ private void ParallelApplySlots(Word inWord, HashSet output) _template, work.Item1, false, - FailureReason.None + FailureReason.None, + null ); add = false; break; @@ -149,7 +154,8 @@ private void ParallelApplySlots(Word inWord, HashSet output) _template, work.Item1, true, - FailureReason.None + FailureReason.None, + null ); outStack.Push(work.Item1); } diff --git a/src/SIL.Machine.Morphology.HermitCrab/AnalysisStratumRule.cs b/src/SIL.Machine.Morphology.HermitCrab/AnalysisStratumRule.cs index 3427cca6b..71a636c64 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/AnalysisStratumRule.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/AnalysisStratumRule.cs @@ -185,7 +185,10 @@ internal IEnumerable Apply(Word input, ref int alternativeCount) private IEnumerable ApplyMorphologicalRules(Word input) { if (_morpher.OwesImpossiblePartial(input)) + { + _morpher.TraceOwedPartialDropped(input); yield break; + } foreach (Word mruleOutWord in _mrulesRule.Apply(input).Distinct(FreezableEqualityComparer.Default)) { switch (_stratum.MorphologicalRuleOrder) @@ -206,7 +209,10 @@ private IEnumerable ApplyMorphologicalRules(Word input) private IEnumerable ApplyTemplates(Word input) { if (_morpher.OwesImpossiblePartial(input)) + { + _morpher.TraceOwedPartialDropped(input); yield break; + } foreach (Word tempOutWord in _templatesRule.Apply(input).Distinct(FreezableEqualityComparer.Default)) { switch (_stratum.MorphologicalRuleOrder) diff --git a/src/SIL.Machine.Morphology.HermitCrab/FinalTemplatePartialReachability.cs b/src/SIL.Machine.Morphology.HermitCrab/FinalTemplatePartialReachability.cs index 6a7cd3ee0..cb285b74c 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/FinalTemplatePartialReachability.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/FinalTemplatePartialReachability.cs @@ -7,7 +7,7 @@ namespace SIL.Machine.Morphology.HermitCrab { /// /// Finds the affix templates that a partial word can reach in synthesis. A word is partial at a template only - /// if a partial affix-process rule applied before it (a partial root takes no template), in the template's + /// if a partial affix-process rule that is neither a template-slot nor a clitic rule applied before it (a partial root takes no template), in the template's /// stratum or an earlier one. Its part of speech is then whatever that rule leaves, changed by any rules /// applied since. The search over-approximates: it tracks only the part-of-speech feature, treats an /// unspecified or variable part of speech as every part of speech, ignores application counts, rule order, @@ -35,7 +35,7 @@ public static HashSet Compute(Language lang) .Distinct() .ToList(); - foreach (AffixProcessRule rule in rules.OfType().Where(r => r.IsPartial)) + foreach (AffixProcessRule rule in rules.OfType().Where(IsSynthesisPartial)) { anyPartial = true; reach.UnionWith( @@ -78,6 +78,13 @@ public static HashSet Compute(Language lang) return result; } + // Synthesis marks a word partial only for a non-template, non-clitic partial rule + // (SynthesisAffixProcessRule), and a slot or clitic rule is exempt from the final-template check. + internal static bool IsSynthesisPartial(AffixProcessRule rule) + { + return rule.IsPartial && !rule.IsTemplateRule && !rule.GetIsCliticRule(); + } + private static IEnumerable ApplyRule( HashSet all, HashSet input, diff --git a/src/SIL.Machine.Morphology.HermitCrab/ITraceManager.cs b/src/SIL.Machine.Morphology.HermitCrab/ITraceManager.cs index e004254de..7653e95b2 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/ITraceManager.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/ITraceManager.cs @@ -28,6 +28,7 @@ public enum FailureReason MaxApplicationCount, AllomorphNotApplicable, LexicalLookupFeatures, + OwedPartialCannotUnapply, } public interface ITraceManager @@ -45,7 +46,13 @@ public interface ITraceManager void PhonologicalRuleNotUnapplied(IPhonologicalRule rule, int subruleIndex, Word input); void BeginUnapplyTemplate(AffixTemplate template, Word input); - void EndUnapplyTemplate(AffixTemplate template, Word output, bool unapplied, FailureReason reason); + void EndUnapplyTemplate( + AffixTemplate template, + Word output, + bool unapplied, + FailureReason reason, + object failureObj + ); void MorphologicalRuleUnapplied(IMorphologicalRule rule, int subruleIndex, Word input, Word output); void MorphologicalRuleNotUnapplied(IMorphologicalRule rule, int subruleIndex, Word input); diff --git a/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs b/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs index 85482a88e..48638713f 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs @@ -72,16 +72,46 @@ public Morpher(ITraceManager traceManager, Language lang, int maxDegreeOfParalle { _partialReachableTemplates = FinalTemplatePartialReachability.Compute(lang); _partialLookAhead = new PartialRuleLookAhead(lang); - _canPruneFinalTemplates = - _partialLookAhead.IsActive - || lang.Strata.SelectMany(s => s.AffixTemplates) - .Any(t => t.IsFinal && !_partialReachableTemplates.Contains(t)); + // Only a final template can arm the prune, so without one nothing is tracked. + _canPruneFinalTemplates = lang + .Strata.SelectMany(s => s.AffixTemplates) + .Any(t => t.IsFinal && (_partialLookAhead.IsActive || !_partialReachableTemplates.Contains(t))); + _partialRules = lang + .Strata.SelectMany(s => + s.MorphologicalRules.Concat( + s.AffixTemplates.SelectMany(t => t.Slots).SelectMany(sl => sl.Rules) + ) + ) + .OfType() + .Where(FinalTemplatePartialReachability.IsSynthesisPartial) + .Distinct() + .ToList(); } } private readonly HashSet _partialReachableTemplates; private readonly PartialRuleLookAhead _partialLookAhead; private readonly bool _canPruneFinalTemplates; + private readonly IReadOnlyList _partialRules; + + /// + /// Traces the drop of an analysis that still owes a partial rule as an unapplied + /// template, with the partial rules as the failure + /// object. + /// + internal void TraceOwedPartialDropped(Word input) + { + if (_traceManager.IsTracing && input.OwedPartialTemplate != null) + { + _traceManager.EndUnapplyTemplate( + input.OwedPartialTemplate, + input, + false, + FailureReason.OwedPartialCannotUnapply, + _partialRules + ); + } + } /// /// Whether analysis records the template state: the prune is on, or some final template can be pruned in a @@ -405,7 +435,10 @@ private IEnumerable SynthesizeSequential(string word, IEnumerable an { // No partial rule was unapplied below the final template that followed a non-template rule. if (analysisWord.NeedsPartial) + { + TraceOwedPartialDropped(analysisWord); continue; + } foreach (Word synthesisWord in LexicalLookup(analysisWord)) { foreach (Word alternative in synthesisWord.ExpandAlternatives()) @@ -440,7 +473,10 @@ private IEnumerable SynthesizeParallel(string word, ConcurrentQueue { analyses.TryDequeue(out Word analysisWord); if (analysisWord.NeedsPartial) + { + TraceOwedPartialDropped(analysisWord); continue; + } foreach (Word synthesisWord in LexicalLookup(analysisWord)) { foreach (Word alternative in synthesisWord.ExpandAlternatives()) diff --git a/src/SIL.Machine.Morphology.HermitCrab/PartialRuleLookAhead.cs b/src/SIL.Machine.Morphology.HermitCrab/PartialRuleLookAhead.cs index e57c9f0aa..ed2b95692 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/PartialRuleLookAhead.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/PartialRuleLookAhead.cs @@ -49,7 +49,7 @@ public PartialRuleLookAhead(Language lang) { AffixProcessRule[] partial = stratum .MorphologicalRules.OfType() - .Where(r => r.IsPartial && !r.IsTemplateRule && !r.GetIsCliticRule()) + .Where(FinalTemplatePartialReachability.IsSynthesisPartial) .ToArray(); if (!partialInDeeperStratum && partial.Length > 0) _material[stratum] = Inspect(stratum, partial); diff --git a/src/SIL.Machine.Morphology.HermitCrab/Trace.cs b/src/SIL.Machine.Morphology.HermitCrab/Trace.cs index d9277a0b0..712266ea1 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/Trace.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/Trace.cs @@ -208,5 +208,12 @@ public IHCRule Source public string OutputText => _outputText ?? FormatText(Output); public FailureReason FailureReason { get; internal set; } + + /// + /// Gets what is about, when it has a subject: for + /// , the partial rules (a list of + /// ) that the analysis owes one of. Otherwise null. + /// + public object FailureObject { get; internal set; } } } diff --git a/src/SIL.Machine.Morphology.HermitCrab/TraceManager.cs b/src/SIL.Machine.Morphology.HermitCrab/TraceManager.cs index a890deb0b..a13ad9af2 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/TraceManager.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/TraceManager.cs @@ -90,11 +90,22 @@ public void BeginUnapplyTemplate(AffixTemplate template, Word input) ((Trace)input.CurrentTrace).Children.Add(Node(TraceType.TemplateAnalysisInput, template, input: input)); } - public void EndUnapplyTemplate(AffixTemplate template, Word output, bool unapplied, FailureReason reason) + public void EndUnapplyTemplate( + AffixTemplate template, + Word output, + bool unapplied, + FailureReason reason, + object failureObj + ) { - ((Trace)output.CurrentTrace).Children.Add( - Node(TraceType.TemplateAnalysisOutput, template, output: unapplied ? output : null, reason: reason) + Trace trace = Node( + TraceType.TemplateAnalysisOutput, + template, + output: unapplied ? output : null, + reason: reason ); + trace.FailureObject = failureObj; + ((Trace)output.CurrentTrace).Children.Add(trace); } public void MorphologicalRuleUnapplied(IMorphologicalRule rule, int subruleIndex, Word input, Word output) diff --git a/src/SIL.Machine.Morphology.HermitCrab/Word.cs b/src/SIL.Machine.Morphology.HermitCrab/Word.cs index b3ab02fc5..f874874dc 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/Word.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/Word.cs @@ -32,6 +32,7 @@ public class Word : Freezable, IAnnotatedData, ICloneable private FinalTemplateState _finalTemplateState; private bool _isPartial; private bool _needsPartial; + private AffixTemplate _owedPartialTemplate; private readonly Dictionary> _disjunctiveAllomorphIndices; private int _mruleAppCount = 0; private readonly IList _alternatives = new List(); @@ -99,6 +100,7 @@ protected Word(Word word) _finalTemplateState = word._finalTemplateState; _isPartial = word._isPartial; _needsPartial = word._needsPartial; + _owedPartialTemplate = word._owedPartialTemplate; CurrentTrace = word.CurrentTrace; _disjunctiveAllomorphIndices = word._disjunctiveAllomorphIndices.ToDictionary( kvp => kvp.Key, @@ -434,6 +436,17 @@ internal bool NeedsPartial } } + /// The final template whose unapplication made this analysis owe a partial rule; for traces only. + internal AffixTemplate OwedPartialTemplate + { + get { return _owedPartialTemplate; } + set + { + CheckFrozen(); + _owedPartialTemplate = value; + } + } + internal FinalTemplateState FinalTemplateState { get { return _finalTemplateState; } diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/PartialRulePruneTests.cs b/tests/SIL.Machine.Morphology.HermitCrab.Tests/PartialRulePruneTests.cs index 8a41c3dc1..91ad9261a 100644 --- a/tests/SIL.Machine.Morphology.HermitCrab.Tests/PartialRulePruneTests.cs +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/PartialRulePruneTests.cs @@ -32,6 +32,9 @@ public class PartialRulePruneTests "cx2", // a flagged and an unflagged word differ only in NeedsPartial and must both survive "cx3", // lexical family and a partial rule: master returns duplicates, see the pinning test below "cx4", // ModifyFromInput changes the partial material + "cx10", // no final template, so no state is tracked + "cx11", // the only partial rule sits in a template slot + "cx12", // the only partial rule is a clitic rule "cx9b", // ModifyFromInput on an unbounded part widens the node the partial rule needs "cx5", // InsertSimpleContext splits the partial material "cx6", // an infix and a copy of a part (reduplication) rearrange the partial material @@ -45,6 +48,16 @@ public class PartialRulePruneTests private static string DataPath(string file) => Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "TestData", "PartialPrune", file); + private static IEnumerable Flatten(Trace trace) + { + yield return trace; + foreach (Trace child in trace.Children) + { + foreach (Trace descendant in Flatten(child)) + yield return descendant; + } + } + private static IEnumerable GrammarCases() { foreach (string grammar in Grammars) @@ -57,11 +70,12 @@ private static IEnumerable GrammarCases() private static Morpher CreateMorpher( string grammar, int maxDegreeOfParallelism, - Func? selector = null + Func? selector = null, + bool tracing = false ) { Language language = XmlLanguageLoader.Load(DataPath(grammar + ".xml")); - var morpher = new Morpher(new TraceManager(), language, maxDegreeOfParallelism); + var morpher = new Morpher(new TraceManager { IsTracing = tracing }, language, maxDegreeOfParallelism); if (DeletionReapplied.Contains(grammar)) morpher.DeletionReapplications = 1; if (selector != null) @@ -170,4 +184,63 @@ public void MorphologicalOutputAction_Subtypes_AreTheOnesPartialRuleLookAheadKno ) ); } + + // A pruned candidate must show in a trace, not vanish: the final template that owes a partial rule, with the + // partial rules as the failure object. + [TestCase("g6", "lopqtr")] + [TestCase("g6b", "loptr")] + [TestCase("cx1b", "lopkotr")] + public void ParseWord_OwedPartialDropped_IsTraced(string grammar, string word) + { + foreach (int degree in new[] { 1, 0 }) + { + Morpher morpher = CreateMorpher(grammar, degree, tracing: true); + morpher.ParseWord(word, out object root); + Trace[] dropped = Flatten((Trace)root) + .Where(t => t.FailureReason == FailureReason.OwedPartialCannotUnapply) + .ToArray(); + Assert.That(dropped, Is.Not.Empty); + foreach (Trace t in dropped) + { + Assert.That(t.Type, Is.EqualTo(TraceType.TemplateAnalysisOutput)); + Assert.That(t.Source, Is.InstanceOf().With.Property("IsFinal").True); + Assert.That(t.Output, Is.Null); + Assert.That(t.FailureObject, Is.InstanceOf>()); + Assert.That((IReadOnlyList)t.FailureObject, Is.Not.Empty); + } + } + } + + // Nothing arms the prune without a final template, so the analysis state must not be tracked. + [TestCase("cx10", false)] + [TestCase("cx1b", true)] + [TestCase("cx11", true)] + [TestCase("cx12", true)] + public void TracksFinalTemplateState_PartialRuleGrammar_OnlyWhenAFinalTemplateCanBePruned( + string grammar, + bool tracked + ) + { + Assert.That(CreateMorpher(grammar, 1).TracksFinalTemplateState, Is.EqualTo(tracked)); + } + + // Partial rules in a template slot or clitic rules never make a word partial in synthesis, so they do not + // keep the final-template prune off. + [TestCase("cx11", "lopkmootr", 15, TestName = "SlotPartialRuleDoesNotKeepPruneOff")] + [TestCase("cx12", "lopkotr", 13, TestName = "CliticPartialRuleDoesNotKeepPruneOff")] + public void ParseWord_OnlyNonPartialWordsReachTheTemplate_Prunes(string grammar, string word, int maxCalls) + { + int calls = 0; + Morpher morpher = CreateMorpher( + grammar, + maxDegreeOfParallelism: 1, + selector: _ => + { + calls++; + return true; + } + ); + Signature(morpher, word); + Assert.That(calls, Is.LessThanOrEqualTo(maxCalls)); + } } diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx10.expected b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx10.expected new file mode 100644 index 000000000..aef9673c9 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx10.expected @@ -0,0 +1,10 @@ +lopko LOP.0+PK.0|lopko|[POS:B] +lopkot LOP.0+PK.0+T.0|lopkot|[POS:B] +lopkotr LOP.0+PK.0+R.0+T.0|lopkotr|[POS:B] +lopt - +loptr - +lopkmoo LOP.0+MO.0+PK.0|lopkmoo|[POS:B] +lopkmoot LOP.0+MO.0+PK.0+T.0|lopkmoot|[POS:B] +lopkmootr LOP.0+MO.0+PK.0+R.0+T.0|lopkmootr|[POS:B] +lopkr - +lopkt - diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx10.words b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx10.words new file mode 100644 index 000000000..63bae33ad --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx10.words @@ -0,0 +1,10 @@ +lopko +lopkot +lopkotr +lopt +loptr +lopkmoo +lopkmoot +lopkmootr +lopkr +lopkt diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx10.xml b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx10.xml new file mode 100644 index 000000000..01d227b0d --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx10.xml @@ -0,0 +1,38 @@ + + + + LaComp + ABE + Main +abdegiklmnopqrstuwxyz + + AnyKO + +Main +pk + +ko +PKpk +t + +t +Tt +r + +r +Rr +cr + + + + +T1slotT1 + +lopLOPlop +kamKAMkam +moMOmo +koKOko + + + + diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx11.expected b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx11.expected new file mode 100644 index 000000000..2619725c9 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx11.expected @@ -0,0 +1,10 @@ +lopko - +lopkot - +lopkotr - +lopt LOP.0+T.0|lopt|[POS:B] +loptr - +lopkmoo - +lopkmoot LOP.0+MO.0+PK.0+T.0|lopkmoot|[POS:B] +lopkmootr - +lopkr - +lopkt - diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx11.words b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx11.words new file mode 100644 index 000000000..63bae33ad --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx11.words @@ -0,0 +1,10 @@ +lopko +lopkot +lopkotr +lopt +loptr +lopkmoo +lopkmoot +lopkmootr +lopkr +lopkt diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx11.xml b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx11.xml new file mode 100644 index 000000000..60097eaed --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx11.xml @@ -0,0 +1,39 @@ + + + + LaComp + ABE + Main +abdegiklmnopqrstuwxyz + + AnyKO + +Main +pk + +ko +PKpk +t + +t +Tt +r + +r +Rr +cr + + + + +T0slotPK +T1slotT1 + +lopLOPlop +kamKAMkam +moMOmo +koKOko + + + + diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx12.expected b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx12.expected new file mode 100644 index 000000000..e63762542 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx12.expected @@ -0,0 +1,10 @@ +lopko - +lopkot LOP.0+PK.0+T.0|lopkot|[POS:B] +lopkotr - +lopt LOP.0+T.0|lopt|[POS:B] +loptr - +lopkmoo - +lopkmoot LOP.0+MO.0+PK.0+T.0|lopkmoot|[POS:B] +lopkmootr - +lopkr - +lopkt - diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx12.words b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx12.words new file mode 100644 index 000000000..63bae33ad --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx12.words @@ -0,0 +1,10 @@ +lopko +lopkot +lopkotr +lopt +loptr +lopkmoo +lopkmoot +lopkmootr +lopkr +lopkt diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx12.xml b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx12.xml new file mode 100644 index 000000000..a67e3e31d --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TestData/PartialPrune/cx12.xml @@ -0,0 +1,38 @@ + + + + LaComp + ABE + Main +abdegiklmnopqrstuwxyz + + AnyKO + +Main +pk + +ko +PKpk +t + +t +Tt +r + +r +Rr +cr + + + + +T1slotT1 + +lopLOPlop +kamKAMkam +moMOmo +koKOko + + + +