diff --git a/src/SIL.Machine.Morphology.HermitCrab/AffixTemplate.cs b/src/SIL.Machine.Morphology.HermitCrab/AffixTemplate.cs index 02e16e8e..3a5aafdb 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/AffixTemplate.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/AffixTemplate.cs @@ -16,6 +16,8 @@ public class AffixTemplate : HCRuleBase private Stratum _stratum; private readonly ObservableCollection _slots; + private bool _isFinal; + /// /// Initializes a new instance of the class. /// @@ -37,6 +39,7 @@ private void SlotsChanged(object sender, NotifyCollectionChangedEventArgs e) { rule.Stratum = null; rule.IsTemplateRule = false; + rule.IsFinalTemplateRule = false; } } } @@ -48,6 +51,7 @@ private void SlotsChanged(object sender, NotifyCollectionChangedEventArgs e) { rule.Stratum = Stratum; rule.IsTemplateRule = true; + rule.IsFinalTemplateRule = IsFinal; } } } @@ -55,7 +59,21 @@ private void SlotsChanged(object sender, NotifyCollectionChangedEventArgs e) public FeatureStruct RequiredSyntacticFeatureStruct { get; set; } - public bool IsFinal { get; set; } + public bool IsFinal + { + get { return _isFinal; } + set + { + _isFinal = value; + foreach (AffixTemplateSlot slot in Slots) + { + foreach (MorphemicMorphologicalRule rule in slot.Rules) + { + rule.IsFinalTemplateRule = value; + } + } + } + } public IList Slots { diff --git a/src/SIL.Machine.Morphology.HermitCrab/AnalysisStateKey.cs b/src/SIL.Machine.Morphology.HermitCrab/AnalysisStateKey.cs index 8575ff40..f4f29d2e 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/AnalysisStateKey.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/AnalysisStateKey.cs @@ -31,6 +31,7 @@ namespace SIL.Machine.Morphology.HermitCrab private readonly FeatureStruct _realizationalFS; private readonly int _nonHeadCount; private readonly IReadOnlyDictionary _ruleCounts; + private readonly bool? _isLastUnappliedRuleNonTemplate; private readonly int _hashCode; /// @@ -62,6 +63,7 @@ private AnalysisStateKey(Word word) _realizationalFS = word.RealizationalFeatureStruct; _nonHeadCount = word.NonHeadCount; _ruleCounts = word.UnappliedRuleCounts; + _isLastUnappliedRuleNonTemplate = word.IsLastUnappliedRuleNonTemplate; // See PinAndKey for why the key pins these rather than just reading them. _shape.Freeze(); @@ -74,6 +76,7 @@ private AnalysisStateKey(Word word) hash = hash * 31 + _syntacticFS.GetFrozenHashCode(); hash = hash * 31 + _realizationalFS.GetFrozenHashCode(); hash = hash * 31 + _nonHeadCount; + hash = hash * 31 + _isLastUnappliedRuleNonTemplate.GetHashCode(); if (_ruleCounts != null) { // XOR rather than the usual *31 rolling combine: the multiset is unordered, so entries @@ -100,6 +103,8 @@ public bool Equals(AnalysisStateKey other) return false; if (!_syntacticFS.ValueEquals(other._syntacticFS) || !_realizationalFS.ValueEquals(other._realizationalFS)) return false; + if (_isLastUnappliedRuleNonTemplate != other._isLastUnappliedRuleNonTemplate) + return false; return RuleCountsEqual(_ruleCounts, other._ruleCounts); } diff --git a/src/SIL.Machine.Morphology.HermitCrab/ITraceManager.cs b/src/SIL.Machine.Morphology.HermitCrab/ITraceManager.cs index 31d8fa49..eda5381b 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/ITraceManager.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/ITraceManager.cs @@ -47,6 +47,13 @@ public interface ITraceManager void MorphologicalRuleUnapplied(IMorphologicalRule rule, int subruleIndex, Word input, Word output); void MorphologicalRuleNotUnapplied(IMorphologicalRule rule, int subruleIndex, Word input); + void MorphologicalRuleNotUnapplied( + IMorphologicalRule rule, + int subruleIndex, + Word input, + FailureReason reason, + object failureObj + ); void CompoundingRuleNotUnapplied( IMorphologicalRule rule, diff --git a/src/SIL.Machine.Morphology.HermitCrab/MorphemicMorphologicalRule.cs b/src/SIL.Machine.Morphology.HermitCrab/MorphemicMorphologicalRule.cs index 0381fe25..08e12f58 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/MorphemicMorphologicalRule.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/MorphemicMorphologicalRule.cs @@ -7,6 +7,7 @@ public abstract class MorphemicMorphologicalRule : Morpheme, IMorphologicalRule { public string Name { get; set; } public bool IsTemplateRule { get; set; } + public bool IsFinalTemplateRule { get; set; } public override MorphemeType MorphemeType { diff --git a/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs b/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs index 54ac7b5e..b1264156 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs @@ -65,6 +65,18 @@ public Morpher(ITraceManager traceManager, Language lang, int maxDegreeOfParalle RuleSelector = rule => true; _morphemes = new ReadOnlyObservableCollection(morphemes); + IsPartial = GetPartialMorphemes().Count() > 0; + } + + public IEnumerable GetPartialMorphemes() + { + var morphemes = new HashSet(); + foreach (Morpheme morpheme in _morphemes) + { + if (morpheme.IsPartial) + morphemes.Add(morpheme); + } + return morphemes; } public ITraceManager TraceManager @@ -88,6 +100,11 @@ public ITraceManager TraceManager /// public bool MergeEquivalentAnalyses { get; set; } + /// + /// A Morpher is partial if any of the elements are partial. + /// + public bool IsPartial { get; set; } + /// /// Caps the concurrency used within a single parse or generation -- analysis cascade, /// affix-template unapplication and synthesis alike. A value of 1 runs the work fully diff --git a/src/SIL.Machine.Morphology.HermitCrab/MorphologicalRules/AnalysisAffixProcessRule.cs b/src/SIL.Machine.Morphology.HermitCrab/MorphologicalRules/AnalysisAffixProcessRule.cs index 4e89fef9..0d05e6ca 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/MorphologicalRules/AnalysisAffixProcessRule.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/MorphologicalRules/AnalysisAffixProcessRule.cs @@ -49,6 +49,23 @@ public IEnumerable Apply(Word input) return Enumerable.Empty(); } + // Do not allow a final template to unapply if the grammar is not partial + // and a non-template was last unapplied. + if (!_morpher.IsPartial && _rule.IsFinalTemplateRule && input.IsLastUnappliedRuleNonTemplate == true) + { + if (_morpher.TraceManager.IsTracing) + { + _morpher.TraceManager.MorphologicalRuleNotUnapplied( + _rule, + -1, + input, + FailureReason.NonPartialRuleProhibitedAfterFinalTemplate, + null + ); + } + return Enumerable.Empty(); + } + var output = new List(); for (int i = 0; i < _rules.Count; i++) { diff --git a/src/SIL.Machine.Morphology.HermitCrab/TraceManager.cs b/src/SIL.Machine.Morphology.HermitCrab/TraceManager.cs index 56a4119a..0335bcbe 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/TraceManager.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/TraceManager.cs @@ -84,6 +84,24 @@ public void MorphologicalRuleNotUnapplied(IMorphologicalRule rule, int subruleIn ); } + public void MorphologicalRuleNotUnapplied( + IMorphologicalRule rule, + int subruleIndex, + Word input, + FailureReason reason, + object failureObj + ) + { + ((Trace)input.CurrentTrace).Children.Add( + new Trace(TraceType.MorphologicalRuleAnalysis, rule) + { + SubruleIndex = subruleIndex, + Input = input, + FailureReason = reason, + } + ); + } + public void CompoundingRuleNotUnapplied( IMorphologicalRule rule, int subruleIndex, diff --git a/src/SIL.Machine.Morphology.HermitCrab/Word.cs b/src/SIL.Machine.Morphology.HermitCrab/Word.cs index 51c3c53c..fe35bd8b 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/Word.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/Word.cs @@ -28,6 +28,7 @@ public class Word : Freezable, IAnnotatedData, ICloneable private FeatureStruct _realizationalFS; private Stratum _stratum; private bool? _isLastAppliedRuleFinal; + private bool? _isLastUnappliedRuleNonTemplate; private bool _isPartial; private readonly Dictionary> _disjunctiveAllomorphIndices; private int _mruleAppCount = 0; @@ -47,6 +48,7 @@ public Word(RootAllomorph rootAllomorph, FeatureStruct realizationalFS) _nonHeadApps = new List(); _obligatorySyntacticFeatures = new IDBearerSet(); _isLastAppliedRuleFinal = null; + _isLastUnappliedRuleNonTemplate = null; _disjunctiveAllomorphIndices = new Dictionary>(); } @@ -65,6 +67,7 @@ public Word(Stratum stratum, Shape shape) _nonHeadApps = new List(); _obligatorySyntacticFeatures = new IDBearerSet(); _isLastAppliedRuleFinal = null; + _isLastUnappliedRuleNonTemplate = null; _isPartial = false; _disjunctiveAllomorphIndices = new Dictionary>(); } @@ -93,6 +96,7 @@ private Word(Word word, bool cloneNonHeadApps) _nonHeadAppIndex = word._nonHeadAppIndex; _obligatorySyntacticFeatures = new IDBearerSet(word._obligatorySyntacticFeatures); _isLastAppliedRuleFinal = word._isLastAppliedRuleFinal; + _isLastUnappliedRuleNonTemplate = word._isLastUnappliedRuleNonTemplate; _isPartial = word._isPartial; CurrentTrace = word.CurrentTrace; AnalysisScope = word.AnalysisScope; @@ -346,6 +350,8 @@ internal void MorphologicalRuleUnapplied(IMorphologicalRule mrule) _mruleApps.Add(mrule); _mruleAppIndex++; } + _isLastUnappliedRuleNonTemplate = + (mrule is MorphemicMorphologicalRule morphRule) && !morphRule.IsTemplateRule; } /// @@ -392,6 +398,16 @@ internal bool? IsLastAppliedRuleFinal } } + internal bool? IsLastUnappliedRuleNonTemplate + { + get { return _isLastUnappliedRuleNonTemplate; } + set + { + CheckFrozen(); + _isLastUnappliedRuleNonTemplate = value; + } + } + /// /// Gets the number of times the specified morphological rule has been applied. /// @@ -621,6 +637,7 @@ protected override int FreezeImpl() code = code * 31 + _mruleApps.GetSequenceHashCode(); code = code * 31 + _mruleAppIndex.GetHashCode(); code = code * 31 + _isLastAppliedRuleFinal.GetHashCode(); + code = code * 31 + _isLastUnappliedRuleNonTemplate.GetHashCode(); return code; } @@ -640,7 +657,8 @@ public override bool ValueEquals(Word other) && _rootAllomorph == other._rootAllomorph && _mruleApps.SequenceEqual(other._mruleApps) && _mruleAppIndex == other._mruleAppIndex - && _isLastAppliedRuleFinal == other._isLastAppliedRuleFinal; + && _isLastAppliedRuleFinal == other._isLastAppliedRuleFinal + && _isLastUnappliedRuleNonTemplate == other._isLastUnappliedRuleNonTemplate; } public Word Clone() diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/AffixTemplateTests.cs b/tests/SIL.Machine.Morphology.HermitCrab.Tests/AffixTemplateTests.cs index 54786c82..696d031f 100644 --- a/tests/SIL.Machine.Morphology.HermitCrab.Tests/AffixTemplateTests.cs +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/AffixTemplateTests.cs @@ -348,6 +348,163 @@ public void NonFinalTemplate() AssertMorphsEqual(morpher.ParseWord("sagdmis"), "32 PAST 53 PL"); } + [Test] + public void EarlyPruningOfFinalTemplate() + { + var any = FeatureStruct.New().Symbol(HCFeatureSystem.Segment).Value; + var alvStop = FeatureStruct + .New(Language.PhonologicalFeatureSystem) + .Symbol(HCFeatureSystem.Segment) + .Symbol("cons+") + .Symbol("strident-") + .Symbol("del_rel-") + .Symbol("alveolar") + .Value; + var voicelessCons = FeatureStruct + .New(Language.PhonologicalFeatureSystem) + .Symbol(HCFeatureSystem.Segment) + .Symbol("cons+") + .Symbol("vd-") + .Value; + + var edSuffix = new AffixProcessRule { Name = "ed_suffix", Gloss = "PAST" }; + edSuffix.Allomorphs.Add( + new AffixProcessAllomorph + { + Lhs = + { + Pattern.New("1").Annotation(any).OneOrMore.Value, + Pattern.New("2").Annotation(alvStop).Value, + }, + Rhs = { new CopyFromInput("1"), new CopyFromInput("2"), new InsertSegments(Table3, "ɯd") }, + } + ); + edSuffix.Allomorphs.Add( + new AffixProcessAllomorph + { + Lhs = { Pattern.New("1").Annotation(any).OneOrMore.Annotation(voicelessCons).Value }, + Rhs = { new CopyFromInput("1"), new InsertSegments(Table3, "t") }, + } + ); + edSuffix.Allomorphs.Add( + new AffixProcessAllomorph + { + Lhs = { Pattern.New("1").Annotation(any).OneOrMore.Value }, + Rhs = { new CopyFromInput("1"), new InsertSegments(Table3, "d") }, + } + ); + + var verbTemplate = new AffixTemplate + { + Name = "verb", + RequiredSyntacticFeatureStruct = FeatureStruct.New(Language.SyntacticFeatureSystem).Symbol("V").Value, + }; + verbTemplate.Slots.Add(new AffixTemplateSlot(edSuffix)); + Morphophonemic.AffixTemplates.Add(verbTemplate); + + var nominalizer = new AffixProcessRule + { + Name = "nominalizer", + Gloss = "NOM", + RequiredSyntacticFeatureStruct = FeatureStruct.New(Language.SyntacticFeatureSystem).Symbol("V").Value, + OutSyntacticFeatureStruct = FeatureStruct.New(Language.SyntacticFeatureSystem).Symbol("N").Value, + }; + nominalizer.Allomorphs.Add( + new AffixProcessAllomorph + { + Lhs = { Pattern.New("1").Annotation(any).OneOrMore.Value }, + Rhs = { new CopyFromInput("1"), new InsertSegments(Table3, "v") }, + } + ); + Morphophonemic.MorphologicalRules.Add(nominalizer); + + var crule = new CompoundingRule + { + Name = "rule1", + HeadRequiredSyntacticFeatureStruct = FeatureStruct.New(Language.SyntacticFeatureSystem).Symbol("V").Value, + NonHeadRequiredSyntacticFeatureStruct = FeatureStruct + .New(Language.SyntacticFeatureSystem) + .Symbol("N") + .Value, + OutSyntacticFeatureStruct = FeatureStruct.New(Language.SyntacticFeatureSystem).Symbol("N").Value, + }; + crule.Subrules.Add( + new CompoundingSubrule + { + HeadLhs = { Pattern.New("head").Annotation(any).OneOrMore.Value }, + NonHeadLhs = { Pattern.New("nonHead").Annotation(any).OneOrMore.Value }, + Rhs = { new CopyFromInput("head"), new InsertSegments(Table3, "+"), new CopyFromInput("nonHead") }, + } + ); + Morphophonemic.MorphologicalRules.Add(crule); + + var sSuffix = new AffixProcessRule + { + Name = "s_suffix", + Gloss = "PL", + RequiredSyntacticFeatureStruct = FeatureStruct.New(Language.SyntacticFeatureSystem).Symbol("N").Value, + }; + sSuffix.Allomorphs.Add( + new AffixProcessAllomorph + { + Lhs = { Pattern.New("1").Annotation(any).OneOrMore.Value }, + Rhs = { new CopyFromInput("1"), new InsertSegments(Table3, "s") }, + } + ); + + var nounTemplate = new AffixTemplate + { + Name = "noun", + RequiredSyntacticFeatureStruct = FeatureStruct.New(Language.SyntacticFeatureSystem).Symbol("N").Value, + }; + nounTemplate.Slots.Add(new AffixTemplateSlot(sSuffix) { Optional = true }); + Morphophonemic.AffixTemplates.Add(nounTemplate); + + // Verify early pruning of final template. + var morpher = new Morpher(TraceManager, Language); + morpher.IsPartial = false; // Override for testing purposes. + TraceManager.IsTracing = true; + AssertMorphsEqual(morpher.ParseWord("sagdv", out object trace)); + Assert.That( + GetFailureDepth((Trace)trace, FailureReason.NonPartialRuleProhibitedAfterFinalTemplate), + Is.EqualTo(2) + ); + AssertMorphsEqual(morpher.ParseWord("sagdvs")); + TraceManager.IsTracing = false; + + // Verify correctness when non-partial and final. + AssertMorphsEqual(morpher.ParseWord("sagd"), "32 PAST"); + AssertMorphsEqual(morpher.ParseWord("sagdv")); + AssertMorphsEqual(morpher.ParseWord("sagdvs")); + AssertMorphsEqual(morpher.ParseWord("sagdmi")); + AssertMorphsEqual(morpher.ParseWord("sagdmis")); + + // Verify correctness when non-partial and non-final. + verbTemplate.IsFinal = false; + morpher = new Morpher(TraceManager, Language); + morpher.IsPartial = false; + AssertMorphsEqual(morpher.ParseWord("sagd")); + AssertMorphsEqual(morpher.ParseWord("sagdv"), "32 PAST NOM"); + AssertMorphsEqual(morpher.ParseWord("sagdvs"), "32 PAST NOM PL"); + AssertMorphsEqual(morpher.ParseWord("sagdmi"), "32 PAST 53"); + AssertMorphsEqual(morpher.ParseWord("sagdmis"), "32 PAST 53 PL"); + } + + private static int GetFailureDepth(Trace trace, FailureReason reason) + { + if (trace == null) + return 0; + if (trace.FailureReason == reason) + return trace.Depth; + foreach (var child in trace.Children) + { + int depth = GetFailureDepth(child, reason); + if (depth > 0) + return depth; + } + return 0; + } + [Test] public void AffixTemplateAppliedAfterMorphologicalRule() {