Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/SIL.Machine.Morphology.HermitCrab/ITraceManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public enum FailureReason
NonPartialRuleProhibitedAfterFinalTemplate,
NonPartialRuleRequiredAfterNonFinalTemplate,
MaxApplicationCount,
AllomorphNotApplicable,
}

public interface ITraceManager
Expand Down
7 changes: 6 additions & 1 deletion src/SIL.Machine.Morphology.HermitCrab/Morpher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -413,13 +413,18 @@ private IEnumerable<Word> SynthesizeParallel(string word, ConcurrentQueue<Word>
return matches.Distinct(FreezableEqualityComparer<Word>.Default);
}

internal IEnumerable<Word> SynthesizeWord(Word input)
{
return _synthesisRule.Apply(input).Where(IsWordValid);
}

internal IEnumerable<RootAllomorph> SearchRootAllomorphs(Stratum stratum, Shape shape)
{
RootAllomorphTrie alloSearcher = _allomorphTries[stratum];
return alloSearcher.Search(shape).Distinct();
}

private IEnumerable<Word> LexicalLookup(Word input)
internal IEnumerable<Word> LexicalLookup(Word input)
{
if (_traceManager.IsTracing)
_traceManager.LexicalLookup(input.Stratum, input);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ out FeatureStruct syntacticFS
: FinalTemplateState.None;
}
}
outWord.MorphologicalRuleUnapplied(_rule);
outWord.MorphologicalRuleUnapplied(_rule, _rule.Allomorphs[i]);
outWord.Freeze();
if (_morpher.TraceManager.IsTracing)
_morpher.TraceManager.MorphologicalRuleUnapplied(_rule, i, input, outWord);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,21 +196,34 @@ public IEnumerable<Word> Apply(Word input)
outWord.MorphologicalRuleApplied(_rule, appliedAllomorphIndices);
appliedAllomorphIndices.Add(i);

if (_rule.Blockable && outWord.CheckBlocking(out Word newWord))
if (input.IsAllomorphApplicable(allo))
Comment thread
claude[bot] marked this conversation as resolved.
{
if (_rule.Blockable && outWord.CheckBlocking(out Word newWord))
{
if (_morpher.TraceManager.IsTracing)
_morpher.TraceManager.Blocked(_rule, newWord);
outWord = newWord;
}
else
{
outWord.Freeze();
}

if (_morpher.TraceManager.IsTracing)
_morpher.TraceManager.Blocked(_rule, newWord);
outWord = newWord;
_morpher.TraceManager.MorphologicalRuleApplied(_rule, i, input, outWord);
output.Add(outWord);
}
else
else if (_morpher.TraceManager.IsTracing)
{
outWord.Freeze();
_morpher.TraceManager.MorphologicalRuleNotApplied(
_rule,
i,
input,
FailureReason.AllomorphNotApplicable,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FieldWorks' FwXmlTraceManager.MorphologicalRuleNotApplied switches on FailureReason and hits default: return; for unknown values. So in Try a Word this allomorph still disappears from the trace. Do you plan a FieldWorks follow-up (a case plus text in FormatHCTrace.xsl), or is hiding it acceptable there?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I plan to add FailureReason.AllmorphNotApplicable to FieldWorks.

null
);
}

if (_morpher.TraceManager.IsTracing)
_morpher.TraceManager.MorphologicalRuleApplied(_rule, i, input, outWord);
output.Add(outWord);

// return all word syntheses that match subrules that are constrained by environments,
// HC violates the disjunctive property of allomorphs here because it cannot check the
// environmental constraints until it has a surface form, we will enforce the disjunctive
Expand Down
23 changes: 20 additions & 3 deletions src/SIL.Machine.Morphology.HermitCrab/Word.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public class Word : Freezable<Word>, IAnnotatedData<ShapeNode>, ICloneable<Word>
private RootAllomorph _rootAllomorph;
private Shape _shape;
private readonly List<IMorphologicalRule> _mruleApps;
private readonly List<Allomorph> _allomorphApps;
Comment thread
claude[bot] marked this conversation as resolved.
private int _mruleAppIndex = -1;
private readonly Dictionary<IMorphologicalRule, int> _mrulesUnapplied;
private readonly Dictionary<IMorphologicalRule, int> _mrulesApplied;
Expand All @@ -42,6 +43,7 @@ public Word(RootAllomorph rootAllomorph, FeatureStruct realizationalFS)
ResetDirty();
SetRootAllomorph(rootAllomorph);
RealizationalFeatureStruct = realizationalFS;
_allomorphApps = new List<Allomorph>();
_mruleApps = new List<IMorphologicalRule>();
_mrulesUnapplied = new Dictionary<IMorphologicalRule, int>();
_mrulesApplied = new Dictionary<IMorphologicalRule, int>();
Expand All @@ -61,6 +63,7 @@ public Word(Stratum stratum, Shape shape)
SyntacticFeatureStruct = new FeatureStruct();
RealizationalFeatureStruct = new FeatureStruct();
_mprFeatures = new MprFeatureSet();
_allomorphApps = new List<Allomorph>();
_mruleApps = new List<IMorphologicalRule>();
_mrulesUnapplied = new Dictionary<IMorphologicalRule, int>();
_mrulesApplied = new Dictionary<IMorphologicalRule, int>();
Expand All @@ -83,6 +86,7 @@ protected Word(Word word)
SyntacticFeatureStruct = word.SyntacticFeatureStruct.Clone();
RealizationalFeatureStruct = word.RealizationalFeatureStruct.Clone();
_mprFeatures = word.MprFeatures.Clone();
_allomorphApps = new List<Allomorph>(word._allomorphApps);
Comment thread
claude[bot] marked this conversation as resolved.
_mruleApps = new List<IMorphologicalRule>(word._mruleApps);
_mruleAppIndex = word._mruleAppIndex;
_mrulesUnapplied = new Dictionary<IMorphologicalRule, int>(word._mrulesUnapplied);
Expand Down Expand Up @@ -247,6 +251,15 @@ internal bool IsMorphologicalRuleApplicable(IMorphologicalRule rule)
return curRule == rule || (curRule == null && rule is CompoundingRule);
}

internal bool IsAllomorphApplicable(Allomorph allomorph)
Comment thread
claude[bot] marked this conversation as resolved.
{
if (_mruleAppIndex < 0)
return false;

Allomorph curAllomorph = _allomorphApps[_mruleAppIndex];
return curAllomorph == null || curAllomorph == allomorph;
}

internal bool HasRemainingRulesFromStratum(Stratum stratum)
{
if (_mruleAppIndex < 0)
Expand Down Expand Up @@ -313,19 +326,21 @@ internal void RemoveMorph(Annotation<ShapeNode> morphAnn)
}

/// <summary>
/// Notifies this word that the specified morphological rule was unapplied. Null
/// Notifies this word that the specified morphological rule and allomorph were unapplied.
/// A null allomorph means that any allomorph is acceptable. A null morphological rule
/// indicates that an unknown compounding rule was unapplied. This is used when
/// generating a compound word, because the compounding rule is usually not known just
/// the non-head allomorph.
/// </summary>
internal void MorphologicalRuleUnapplied(IMorphologicalRule mrule)
internal void MorphologicalRuleUnapplied(IMorphologicalRule mrule, Allomorph allomorph = null)
Comment thread
claude[bot] marked this conversation as resolved.
{
CheckFrozen();
if (mrule != null)
_mrulesUnapplied.UpdateValue(mrule, () => 0, count => count + 1);
if (!(mrule is RealizationalAffixProcessRule))
{
_mruleApps.Add(mrule);
_allomorphApps.Add(allomorph);
_mruleAppIndex++;
}
}
Expand Down Expand Up @@ -441,7 +456,7 @@ internal IList<Word> ExpandAlternatives()
// Add new rules to alternative.
int m_start = Source == null ? 0 : Source._mruleApps.Count();
for (int i = m_start; i < _mruleApps.Count(); i++)
alternative.MorphologicalRuleUnapplied(_mruleApps[i]);
alternative.MorphologicalRuleUnapplied(_mruleApps[i], _allomorphApps[i]);
int nh_start = Source == null ? 0 : Source._nonHeadApps.Count();
for (int i = nh_start; i < _nonHeadApps.Count(); i++)
alternative.NonHeadUnapplied(_nonHeadApps[i]);
Expand Down Expand Up @@ -536,6 +551,7 @@ protected override int FreezeImpl()
code = code * 31 + _nonHeadAppIndex.GetHashCode();
code = code * 31 + _stratum.GetHashCode();
code = code * 31 + (_rootAllomorph == null ? 0 : _rootAllomorph.GetHashCode());
code = code * 31 + _allomorphApps.GetSequenceHashCode();
code = code * 31 + _mruleApps.GetSequenceHashCode();
code = code * 31 + _mruleAppIndex.GetHashCode();
code = code * 31 + _isLastAppliedRuleFinal.GetHashCode();
Expand All @@ -560,6 +576,7 @@ public override bool ValueEquals(Word other)
&& _nonHeadAppIndex == other._nonHeadAppIndex
&& _stratum == other._stratum
&& _rootAllomorph == other._rootAllomorph
&& _allomorphApps.SequenceEqual(other._allomorphApps)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since _allomorphApps is now part of equality, analyses that differ only in which same-form allomorph was unapplied no longer merge, so each one counts against MaxAlternatives. In a probe test (one suffix, three identical +d allomorphs, parse sagd), the smallest MaxAlternatives that parses went from 1 to 3, with merging on or off. The results are the same. Is that intended? FieldWorks defaults the limit to 0, so only projects that set it are affected, but it would be worth a line in the description.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hadn't thought of that, but I'd like to keep it as is. One of my goals is that synthesis not do any search. If the morphemes are underspecified, then synthesis of one Word could be exponential in the number of morphemes it has. I want it to be linear in the number of morphemes, if possible (or polynomial at worst).

&& _mruleApps.SequenceEqual(other._mruleApps)
&& _mruleAppIndex == other._mruleAppIndex
&& _isLastAppliedRuleFinal == other._isLastAppliedRuleFinal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2031,4 +2031,61 @@ public void NonContiguousRules()
var morpher = new Morpher(TraceManager, Language);
AssertMorphsEqual(morpher.ParseWord("katibɯd"), "49 PER.ACT");
}

[Test]
public void IsAllomorphApplicable()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test builds the analysis Word by hand, so it doesn't check that ParseWord hands the right allomorph to synthesis. It also doesn't cover the cases fixed in review: the disjunctive break, same-form allomorphs differing by MPR feature, and the merged ExpandAlternatives path. Could you add one ParseWord-level test, e.g. -s requiring MPR feature F plus an elsewhere -s, with merging on and off? It would also let SynthesizeWord and LexicalLookup (Morpher.cs:416, :427) stay private. MergeEquivalentAnalyses = true on line 2063 is already the default.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that I understand what you want. I'm happy for you to upgrade the test after I merge it if you want.

{
FeatureStruct any = FeatureStruct.New().Symbol(HCFeatureSystem.Segment).Value;
var sSuffix = new AffixProcessRule
{
Name = "s_suffix",
Gloss = "3SG",
RequiredSyntacticFeatureStruct = FeatureStruct.New(Language.SyntacticFeatureSystem).Symbol("V").Value,
OutSyntacticFeatureStruct = FeatureStruct
.New(Language.SyntacticFeatureSystem)
.Feature(Head)
.EqualTo(head => head.Feature("pers").EqualTo("3"))
.Value,
};
var allomorph1 = new AffixProcessAllomorph
{
Lhs = { Pattern<Word, ShapeNode>.New("1").Annotation(any).OneOrMore.Value },
Rhs = { new CopyFromInput("1"), new InsertSegments(Table3, "s") },
};
var allomorph2 = new AffixProcessAllomorph
{
Lhs = { Pattern<Word, ShapeNode>.New("1").Annotation(any).OneOrMore.Value },
Rhs = { new CopyFromInput("1"), new InsertSegments(Table3, "z") },
};
sSuffix.Allomorphs.Add(allomorph1);
sSuffix.Allomorphs.Add(allomorph2);
Morphophonemic.MorphologicalRules.Add(sSuffix);
var morpher = new Morpher(TraceManager, Language) { MergeEquivalentAnalyses = true };

Shape shape = Morphophonemic.CharacterDefinitionTable.Segment("sag");
var input = new Word(Morphophonemic, shape);
input.MorphologicalRuleUnapplied(sSuffix);
input.Freeze();
Word word = morpher.LexicalLookup(input).First();
IEnumerable<Word> output = morpher.SynthesizeWord(word);
Assert.That(output.Count(), Is.EqualTo(2));

shape = Morphophonemic.CharacterDefinitionTable.Segment("sag");
input = new Word(Morphophonemic, shape);
input.MorphologicalRuleUnapplied(sSuffix, allomorph1);
input.Freeze();
word = morpher.LexicalLookup(input).First();
output = morpher.SynthesizeWord(word);
Assert.That(output.Count(), Is.EqualTo(1));
Assert.That(output.First().ToString(), Is.EqualTo("sags"));

shape = Morphophonemic.CharacterDefinitionTable.Segment("sag");
input = new Word(Morphophonemic, shape);
input.MorphologicalRuleUnapplied(sSuffix, allomorph2);
input.Freeze();
word = morpher.LexicalLookup(input).First();
output = morpher.SynthesizeWord(word);
Assert.That(output.Count(), Is.EqualTo(1));
Assert.That(output.First().ToString(), Is.EqualTo("sagz"));
}
}
Loading