Skip to content
10 changes: 2 additions & 8 deletions src/SIL.Machine.Morphology.HermitCrab/AnalysisStratumRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ internal IEnumerable<Word> Apply(Word input, ref int alternativeCount)
Word origInput = input;
input = input.Clone();
input.Stratum = _stratum;
if (_morpher.IsBlockingStratum(_stratum))
input.FinalTemplateState = FinalTemplateState.None;

_prulesRule.Apply(input);
input.Freeze();
Expand Down Expand Up @@ -161,14 +163,6 @@ internal IEnumerable<Word> Apply(Word input, ref int alternativeCount)
}
}
Word newMruleOutWord = mruleOutWord;
Comment thread
claude[bot] marked this conversation as resolved.
if (mruleOutWord.FinalTemplateState != FinalTemplateState.None)
{
// Clear FinalTemplateState to allow clitics.
newMruleOutWord = mruleOutWord.Clone();
newMruleOutWord.Source = origInput;
newMruleOutWord.FinalTemplateState = FinalTemplateState.None;
newMruleOutWord.Freeze();
}
// Only cache a canonical that made it into the output. Two words can have different keys yet
// be Word.ValueEquals (UnappliedRuleCounts also counts realizational rules, which never enter
// _mruleApps), and a rejected canonical would swallow every later word with its key.
Expand Down
2 changes: 2 additions & 0 deletions src/SIL.Machine.Morphology.HermitCrab/HermitCrabInput.dtd
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@
id ID #REQUIRED
isActive (yes | no) "yes"
blockable (true | false) "true"
isCliticRule (true | false) #IMPLIED
multipleApplication (0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9) "1"
requiredPartsOfSpeech IDREFS #IMPLIED
requiredSubcategorizedRules IDREFS #IMPLIED
Expand All @@ -364,6 +365,7 @@
id ID #REQUIRED
isActive (yes | no) "yes"
blockable (true | false) "true"
isCliticRule (true | false) #IMPLIED
>

<!--
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ public abstract class MorphemicMorphologicalRule : Morpheme, IMorphologicalRule
public string Name { get; set; }
public bool IsTemplateRule { get; set; }

/// <summary>
/// If IsCliticRule is not set, GetIsCliticRule() will return whether the rule is in the "Clitics" stratum.
/// </summary>
public bool? IsCliticRule { get; set; }

internal bool GetIsCliticRule()
{
if (IsCliticRule == null)
return Stratum != null && Stratum.Name == "Clitics";
return (bool)IsCliticRule;
}

public override MorphemeType MorphemeType
{
get { return MorphemeType.Affix; }
Expand Down
15 changes: 15 additions & 0 deletions src/SIL.Machine.Morphology.HermitCrab/Morpher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -812,5 +812,20 @@ int rightIndex
}
}
}

/// <summary>
/// Does this stratum block final templates?
/// </summary>
internal bool IsBlockingStratum(Stratum stratum)
{
if (stratum.PhonologicalRules.Count > 0)
return true;
for (int i = _lang.Strata.Count - 1; i >= 0; i--)
{
if (RuleSelector(_lang.Strata[i]))
return stratum == _lang.Strata[i];
}
return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,12 @@ out FeatureStruct syntacticFS
}
if (!_morpher.IsPartial || _morpher.AlwaysEnforceFinalTemplates)
{
outWord.FinalTemplateState = !_rule.IsTemplateRule
? FinalTemplateState.NonTemplate
: FinalTemplateState.None;
if (!_rule.GetIsCliticRule())
{
outWord.FinalTemplateState = !_rule.IsTemplateRule
? FinalTemplateState.NonTemplate
: FinalTemplateState.None;
}
}
outWord.MorphologicalRuleUnapplied(_rule);
outWord.Freeze();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public IEnumerable<Word> Apply(Word input)
// do not allow a non-partial rule to apply unless the input is partial
if (
!_rule.IsTemplateRule
&& !_rule.GetIsCliticRule()
&& (input.IsLastAppliedRuleFinal ?? false)
&& ((!input.IsPartial && !_rule.IsPartial) || _morpher.AlwaysEnforceFinalTemplates)
)
Expand All @@ -83,6 +84,7 @@ public IEnumerable<Word> Apply(Word input)
// only allow a non-partial rule to apply unless the input is partial
if (
!_rule.IsTemplateRule
&& !_rule.GetIsCliticRule()
&& input.IsLastAppliedRuleFinal.HasValue
&& !input.IsLastAppliedRuleFinal.Value
&& !input.IsPartial
Expand Down Expand Up @@ -183,7 +185,7 @@ public IEnumerable<Word> Apply(Word input)
foreach (Feature obligFeature in _rule.ObligatorySyntacticFeatures)
outWord.ObligatorySyntacticFeatures.Add(obligFeature);

if (!_rule.IsTemplateRule)
if (!_rule.IsTemplateRule && !_rule.GetIsCliticRule())
{
if (_rule.IsPartial)
outWord.IsPartial = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ public IEnumerable<Word> Apply(Word input)
else
{
Word word = input;
if (!word.IsLastAppliedRuleFinal.HasValue || !word.IsLastAppliedRuleFinal.Value)
if (
applicableTemplate
&& (!word.IsLastAppliedRuleFinal.HasValue || !word.IsLastAppliedRuleFinal.Value)
)
{
word = input.Clone();
word.IsLastAppliedRuleFinal = true;
Expand Down
12 changes: 11 additions & 1 deletion src/SIL.Machine.Morphology.HermitCrab/SynthesisStratumRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,17 @@ public IEnumerable<Word> Apply(Word input)
var output = new HashSet<Word>(FreezableEqualityComparer<Word>.Default);
foreach (Word mruleOutWord in ApplyMorphologicalRules(input).Concat(ApplyTemplates(input)))
{
if (!(mruleOutWord.IsLastAppliedRuleFinal ?? false))
if (!_morpher.IsBlockingStratum(_stratum))
{
// Don't close off the words yet.
Word newWord = mruleOutWord.Clone();
Comment thread
jtmaxwell3 marked this conversation as resolved.
newWord.Freeze();
if (_morpher.TraceManager.IsTracing)
_morpher.TraceManager.EndApplyStratum(_stratum, newWord);
output.Add(newWord);
continue;
}
if (!(mruleOutWord.IsLastAppliedRuleFinal ?? true))
{
if (_morpher.TraceManager.IsTracing)
_morpher.TraceManager.NonFinalTemplateAppliedLast(_stratum, mruleOutWord);
Expand Down
2 changes: 2 additions & 0 deletions src/SIL.Machine.Morphology.HermitCrab/XmlLanguageLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,7 @@ out IMorphologicalRule mrule
Id = (string)mruleElem.Element("MorphemeId"),
Gloss = (string)mruleElem.Element("Gloss"),
Blockable = (bool?)mruleElem.Attribute("blockable") ?? true,
IsCliticRule = (bool?)mruleElem.Attribute("isCliticRule"),
IsPartial = (bool?)mruleElem.Attribute("partial") ?? false,
};
var multApp = (string)mruleElem.Attribute("multipleApplication");
Expand Down Expand Up @@ -957,6 +958,7 @@ out IMorphologicalRule mrule
Id = (string)realRuleElem.Element("MorphemeId"),
Gloss = (string)realRuleElem.Element("Gloss"),
Blockable = (bool?)realRuleElem.Attribute("blockable") ?? true,
IsCliticRule = (bool?)realRuleElem.Attribute("isCliticRule"),
};

var fs = new FeatureStruct();
Expand Down
4 changes: 4 additions & 0 deletions src/SIL.Machine.Morphology.HermitCrab/XmlLanguageWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,8 @@ Dictionary<IMorphologicalRule, string> mrules

if (!affixProcessRule.Blockable)
ruleElem.Add(new XAttribute("blockable", "false"));
if (affixProcessRule.IsCliticRule != null)
ruleElem.Add(new XAttribute("isCliticRule", affixProcessRule.IsCliticRule == true ? "true" : "false"));

if (affixProcessRule.MaxApplicationCount != 1)
ruleElem.Add(new XAttribute("multipleApplication", affixProcessRule.MaxApplicationCount));
Expand Down Expand Up @@ -761,6 +763,8 @@ Dictionary<IMorphologicalRule, string> mrules

if (!realRule.Blockable)
ruleElem.Add(new XAttribute("blockable", "false"));
if (realRule.IsCliticRule != null)
ruleElem.Add(new XAttribute("isCliticRule", realRule.IsCliticRule == true ? "true" : "false"));

ruleElem.Add(new XElement("Name", Normalize(realRule.Name)));

Expand Down
117 changes: 117 additions & 0 deletions tests/SIL.Machine.Morphology.HermitCrab.Tests/AffixTemplateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,17 @@ public void NonFinalTemplate()
AssertMorphsEqual(morpher.ParseWord("sagdvs"), "32 PAST NOM PL");
AssertMorphsEqual(morpher.ParseWord("sagdmi"), "32 PAST 53");
AssertMorphsEqual(morpher.ParseWord("sagdmis"), "32 PAST 53 PL");

// Test rules split between two strata.
Comment thread
claude[bot] marked this conversation as resolved.
Morphophonemic.MorphologicalRules.Remove(crule);
Allophonic.MorphologicalRules.Add(crule);
Comment thread
claude[bot] marked this conversation as resolved.
verbTemplate.IsFinal = true;
morpher = new Morpher(TraceManager, Language, 1);
AssertMorphsEqual(morpher.ParseWord("sagdbupu"));

verbTemplate.IsFinal = false;
morpher = new Morpher(TraceManager, Language, 1);
AssertMorphsEqual(morpher.ParseWord("sagdbupu"), "32 PAST 46");
}

[Test]
Expand Down Expand Up @@ -554,6 +565,112 @@ public void AffixTemplateAppliedAfterMorphologicalRule()
AssertMorphsEqual(morpher.ParseWord("sagvs"), "32 NOM PL");
}

[TestCase(true)]
[TestCase(false)]
public void CliticAppliedAfterFinalTemplate(bool alwaysEnforceFinalTemplates)
{
var clitics = new Stratum(Table3)
{
Name = "Clitics",
MorphologicalRuleOrder = MorphologicalRuleOrder.Unordered,
};
Language.Strata.Add(clitics);
try
{
CliticAppliedAfterFinalTemplate(clitics, alwaysEnforceFinalTemplates);
}
finally
{
Language.Strata.Remove(clitics);
}
}

private void CliticAppliedAfterFinalTemplate(Stratum clitics, bool alwaysEnforceFinalTemplates)
{
FeatureStruct any = FeatureStruct.New().Symbol(HCFeatureSystem.Segment).Value;

var edSuffix = new AffixProcessRule { Name = "ed_suffix", Gloss = "PAST" };
edSuffix.Allomorphs.Add(
new AffixProcessAllomorph
{
Lhs = { Pattern<Word, ShapeNode>.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 clitic = new AffixProcessRule
{
Name = "clitic",
Gloss = "PL",
RequiredSyntacticFeatureStruct = FeatureStruct.New(Language.SyntacticFeatureSystem).Symbol("V").Value,
};
clitic.Allomorphs.Add(
new AffixProcessAllomorph
{
Lhs = { Pattern<Word, ShapeNode>.New("1").Annotation(any).OneOrMore.Value },
Rhs = { new CopyFromInput("1"), new InsertSegments(Table3, "s") },
}
);
Morphophonemic.MorphologicalRules.Add(clitic);
var morpher = new Morpher(TraceManager, Language);
morpher.AlwaysEnforceFinalTemplates = alwaysEnforceFinalTemplates;
AssertMorphsEqual(morpher.ParseWord("sagd"), "32 PAST");
AssertMorphsEqual(morpher.ParseWord("sagds"));

clitic.IsCliticRule = true;
morpher = new Morpher(TraceManager, Language);
morpher.AlwaysEnforceFinalTemplates = alwaysEnforceFinalTemplates;
AssertMorphsEqual(morpher.ParseWord("sagd"), "32 PAST");
AssertMorphsEqual(morpher.ParseWord("sagds"), "32 PAST PL");

clitic.IsCliticRule = null;
Morphophonemic.MorphologicalRules.Remove(clitic);
clitics.MorphologicalRules.Add(clitic);
morpher = new Morpher(TraceManager, Language);
morpher.AlwaysEnforceFinalTemplates = alwaysEnforceFinalTemplates;
AssertMorphsEqual(morpher.ParseWord("sagd"), "32 PAST");
AssertMorphsEqual(morpher.ParseWord("sagds"), "32 PAST PL");

clitic.IsCliticRule = false;
morpher = new Morpher(TraceManager, Language);
morpher.AlwaysEnforceFinalTemplates = alwaysEnforceFinalTemplates;
AssertMorphsEqual(morpher.ParseWord("sagd"), "32 PAST");
AssertMorphsEqual(morpher.ParseWord("sagds"));

// Test stacked clitics.
var clitic2 = new AffixProcessRule
{
Name = "clitic2",
Gloss = "T",
RequiredSyntacticFeatureStruct = FeatureStruct.New(Language.SyntacticFeatureSystem).Symbol("V").Value,
};
clitic2.Allomorphs.Add(
new AffixProcessAllomorph
{
Lhs = { Pattern<Word, ShapeNode>.New("1").Annotation(any).OneOrMore.Value },
Rhs = { new CopyFromInput("1"), new InsertSegments(Table3, "t") },
}
);
clitics.MorphologicalRules.Add(clitic2);
clitic.IsCliticRule = true;
morpher = new Morpher(TraceManager, Language);
morpher.AlwaysEnforceFinalTemplates = alwaysEnforceFinalTemplates;
AssertMorphsEqual(morpher.ParseWord("sagdst"), "32 PAST PL T");

clitic.IsCliticRule = false;
morpher = new Morpher(TraceManager, Language);
morpher.AlwaysEnforceFinalTemplates = alwaysEnforceFinalTemplates;
AssertMorphsEqual(morpher.ParseWord("sagdst"));
}

[TestCase(true, TestName = "SameRuleUsedInMultipleTemplates_AffixHasRequiredFeatures")]
[TestCase(false, TestName = "SameRuleUsedInMultipleTemplates_AffixHasNoRequiredFeatures")]
public void SameRuleUsedInMultipleTemplates(bool affixHasRequiredFeatures)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@
<FeatureValue feature="ffeat1" symbolValues="fval1" />
</NonHeadRequiredFootFeatures>
</CompoundingRule>
<MorphologicalRule id="mrule2" blockable="false" multipleApplication="2" requiredPartsOfSpeech="pos1" requiredStemName="stemName1" outputPartOfSpeech="pos2" outputObligatoryFeatures="hfeat1" partial="true">
<MorphologicalRule id="mrule2" blockable="false" isCliticRule="true" multipleApplication="2" requiredPartsOfSpeech="pos1" requiredStemName="stemName1" outputPartOfSpeech="pos2" outputObligatoryFeatures="hfeat1" partial="true">
<Name>morph rule 1</Name>
<MorphologicalSubrules>
<MorphologicalSubrule id="msubrule1">
Expand Down
Loading