diff --git a/src/SIL.Machine.Morphology.HermitCrab/AnalysisAffixTemplateRule.cs b/src/SIL.Machine.Morphology.HermitCrab/AnalysisAffixTemplateRule.cs
index d46b5971b..1f95113d0 100644
--- a/src/SIL.Machine.Morphology.HermitCrab/AnalysisAffixTemplateRule.cs
+++ b/src/SIL.Machine.Morphology.HermitCrab/AnalysisAffixTemplateRule.cs
@@ -93,12 +93,13 @@ private bool RequiredSlotCannotUnapply(Word word, int lastSlot, ref int[] fired)
long unifications = 0;
bool cannot = false;
+ SlotWalk walk = SlotWalk.Start();
for (int j = lastSlot; j >= 0; j--)
{
if (
!_template.Slots[j].Optional
&& !IsFired(fired, j)
- && _lookahead.CannotUnapply(j, word.Shape, ref unifications)
+ && _lookahead.CannotUnapply(j, word.Shape, ref walk, ref unifications)
)
{
if (mode == RequiredSlotLookaheadMode.On)
@@ -109,6 +110,7 @@ private bool RequiredSlotCannotUnapply(Word word, int lastSlot, ref int[] fired)
fired = fired == null ? new[] { j } : fired.Append(j).ToArray();
_morpher.RequiredSlotShadow?.Invoke("fired", _template, j);
}
+ _lookahead.Advance(j, ref walk);
if (!_lookahead.IsPure(j))
break;
}
diff --git a/src/SIL.Machine.Morphology.HermitCrab/RequiredSlotMaterial.cs b/src/SIL.Machine.Morphology.HermitCrab/RequiredSlotMaterial.cs
index 27e60caa6..f7e9cfdc3 100644
--- a/src/SIL.Machine.Morphology.HermitCrab/RequiredSlotMaterial.cs
+++ b/src/SIL.Machine.Morphology.HermitCrab/RequiredSlotMaterial.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using System.Linq;
using SIL.Machine.Annotations;
@@ -22,26 +23,73 @@ internal enum RequiredSlotLookaheadMode
On,
}
+ ///
+ /// What a walk over the slots of one template has established about the shape, in the ordinals of the shape
+ /// the walk started from. Create it with .
+ ///
+ internal struct SlotWalk
+ {
+ /// Stands for "any number" in and .
+ public const int Unbounded = int.MaxValue / 4;
+
+ /// Every node the slots still to unapply can match lies after this ordinal.
+ public int Lo;
+
+ /// Every node the slots still to unapply can match lies before this ordinal.
+ public int Hi;
+
+ /// The most nodes that the slots walked so far can have removed from the left edge.
+ public int ReachLeft;
+
+ /// The most nodes that the slots walked so far can have removed from the right edge.
+ public int ReachRight;
+
+ /// False once a slot walked so far can reorder the nodes it copies.
+ public bool Ordered;
+
+ public static SlotWalk Start()
+ {
+ return new SlotWalk
+ {
+ Lo = -1,
+ Hi = int.MaxValue,
+ Ordered = true,
+ };
+ }
+ }
+
///
/// A necessary condition for a required affix template slot to unapply later in a template walk: some
- /// allomorph of some rule in the slot must have every inserted segment unify with a segment of the shape.
+ /// allomorph of some rule in the slot must have its inserted segments, in the order it inserts them, unify
+ /// with distinct segment nodes of the shape in that order, and the nodes must be where the slots that unapply
+ /// first can leave them.
///
///
/// The tables are built once from the template and depend only on the grammar.
///
- /// Soundness. The analysis pattern of an allomorph holds one top-level constraint per inserted segment
- /// (, ),
- /// and the rule can only unapply if each constraint unifies with a distinct segment node of the shape.
- /// A rule is pure when every right-hand side action copies an input part or inserts material, and every
- /// left-hand part is copied by a that must capture a segment, or has no
- /// segment to restore. Then only copies input
- /// nodes and never untruncates a segment. Unification can only narrow the feature structure of a copied
- /// node, so a constraint that unifies with no node of the shape unifies with no node of the shape after
- /// any number of pure unapplications. Inside one template walk only the rules of that template's slots
- /// run, and the stratum's phonological rules ran before the walk. The test for slot j is therefore used
- /// only when every rule in the slots still to be walked after j is pure. Optional nodes count as present
- /// because the matcher can match them. Anything that cannot be summarised (a null affix, a constraint with
- /// variables, an unknown rule type) leaves the slot unprunable.
+ /// Presence. The analysis pattern of an allomorph holds one top-level constraint per inserted segment, in the
+ /// order of the right-hand side (,
+ /// ), and the rule can only unapply if the constraints
+ /// unify with segment nodes of the shape in that order, each node used once. A rule is pure when every
+ /// right-hand side action copies an input part or inserts material, and every left-hand part is copied by a
+ /// that must capture a segment, or has no segment to restore. Then
+ /// only copies input nodes and never untruncates a
+ /// segment. Unification can only narrow the feature structure of a copied node, so a constraint that unifies
+ /// with no node of the shape unifies with no node of the shape after any number of pure unapplications. Inside
+ /// one template walk only the rules of that template's slots run, and the stratum's phonological rules ran
+ /// before the walk. The test for slot j is therefore used only when every rule in the slots still to be walked
+ /// after j is pure. Optional nodes count as present because the matcher can match them. Anything that cannot
+ /// be summarised (a null affix, a constraint with variables that is then matched by any segment, an unknown
+ /// rule type) can only weaken the test.
+ ///
+ /// Order. A pure rule is ordered when it copies each part at most once and in left-hand order, so the shape
+ /// it unapplies to is a subsequence of the shape it was given. An ordered rule whose inserts all come before
+ /// its first copied part (after its last) is anchored on that side: the analysis matcher is anchored at both
+ /// ends of the shape, so the first inserted constraint must match the first segment left once the outer slots
+ /// have removed their own prefixes. A rule can remove from an edge at most as many nodes as it inserts there
+ /// (), and the nodes of a required ordered slot lie after the prefix and before
+ /// the suffix it inserts (, ). Both only hold while every
+ /// slot walked so far is ordered, and the reach only while none can remove from the middle.
///
internal class RequiredSlotMaterial
{
@@ -61,70 +109,184 @@ public bool IsPure(int slot)
return _slots[slot].IsPure;
}
+ ///
+ /// Records that the slot is the next one to unapply, so that the slots before it are tested against what
+ /// it can leave of the shape.
+ ///
+ public void Advance(int slot, ref SlotWalk walk)
+ {
+ SlotSummary summary = _slots[slot];
+ if (!walk.Ordered || !summary.IsOrdered)
+ {
+ walk.Ordered = false;
+ walk.Lo = -1;
+ walk.Hi = int.MaxValue;
+ walk.ReachLeft = SlotWalk.Unbounded;
+ walk.ReachRight = SlotWalk.Unbounded;
+ return;
+ }
+ walk.ReachLeft = Math.Min(SlotWalk.Unbounded, walk.ReachLeft + summary.ReachLeft);
+ walk.ReachRight = Math.Min(SlotWalk.Unbounded, walk.ReachRight + summary.ReachRight);
+ }
+
///
/// True if no allomorph of the slot can unapply to a word with this shape, provided that every slot
- /// walked between this word and the slot is pure.
+ /// walked between this word and the slot is pure. When the slot can unapply and is ordered, narrows the
+ /// window of the walk for the slots before it.
///
/// The slot to test.
/// The shape of the partial.
+ /// What the slots walked since the shape was taken have established.
/// Incremented by the feature structure unifications performed.
- public bool CannotUnapply(int slot, Shape shape, ref long unifications)
+ public bool CannotUnapply(int slot, Shape shape, ref SlotWalk walk, ref long unifications)
{
SlotSummary summary = _slots[slot];
if (summary.MayAlwaysUnapply)
return false;
- // The constraints of a slot repeat across its allomorphs, so each distinct one is looked up once.
- ulong known = 0;
- ulong present = 0;
- foreach (int[] requirements in summary.Allomorphs)
+ // The window is only narrowed for a slot that has slots after it in the walk, and that needs every
+ // passing allomorph, not the first.
+ bool narrow = walk.Ordered && summary.IsOrdered && slot > 0;
+ int newLo = int.MaxValue;
+ int newHi = -1;
+ bool any = false;
+ foreach (AllomorphSummary allomorph in summary.Allomorphs)
{
- bool all = true;
- foreach (int index in requirements)
+ int prefixEnd = -1;
+ int suffixStart = int.MaxValue;
+ bool found = walk.Ordered
+ ? Embeds(allomorph, shape, ref walk, ref unifications, out prefixEnd, out suffixStart)
+ : AllPresent(allomorph, shape, ref unifications);
+ if (!found)
+ continue;
+
+ if (!narrow)
+ return false;
+ any = true;
+ newLo = Math.Min(newLo, allomorph.Pre > 0 ? prefixEnd : walk.Lo);
+ newHi = Math.Max(newHi, allomorph.Post > 0 ? suffixStart : walk.Hi);
+ }
+ if (!any)
+ return true;
+
+ walk.Lo = newLo;
+ walk.Hi = newHi;
+ return false;
+ }
+
+ // The ordinals are those of the shape, margins excluded. A node is counted against the reach only when it
+ // cannot be skipped, because the matcher may skip an optional node.
+ private static bool Embeds(
+ AllomorphSummary allomorph,
+ Shape shape,
+ ref SlotWalk walk,
+ ref long unifications,
+ out int prefixEnd,
+ out int suffixStart
+ )
+ {
+ prefixEnd = -1;
+ suffixStart = int.MaxValue;
+ FeatureStruct[] sequence = allomorph.Sequence;
+ int front = sequence.Length - allomorph.Post;
+ int forwardEnd = -1;
+
+ int next = 0;
+ int before = 0;
+ int ordinal = -1;
+ foreach (ShapeNode node in shape)
+ {
+ if (next == front)
+ break;
+ ordinal++;
+ if (ordinal >= walk.Hi)
+ break;
+
+ bool segment = node.Type() == HCFeatureSystem.Segment;
+ if (segment && ordinal > walk.Lo)
{
- bool found;
- ulong bit = index < 64 ? 1UL << index : 0;
- if ((known & bit) != 0)
+ if (next < allomorph.Pre && before > Math.Min(SlotWalk.Unbounded, walk.ReachLeft + next))
+ return false;
+
+ FeatureStruct required = sequence[next];
+ if (required == null || IsUnifiable(node, required, ref unifications))
{
- found = (present & bit) != 0;
+ if (next == allomorph.Pre - 1)
+ prefixEnd = ordinal;
+ forwardEnd = ordinal;
+ next++;
}
- else
+ }
+ if (segment && !node.Annotation.Optional)
+ before++;
+ }
+ if (next < front)
+ return false;
+ if (allomorph.Post == 0)
+ return true;
+
+ int taken = 0;
+ int after = 0;
+ ordinal = shape.Count;
+ for (ShapeNode node = shape.Last; node != null && taken < allomorph.Post; node = node.Prev)
+ {
+ ordinal--;
+ if (ordinal <= walk.Lo || ordinal <= forwardEnd)
+ break;
+
+ bool segment = node.Type() == HCFeatureSystem.Segment;
+ if (segment && ordinal < walk.Hi)
+ {
+ if (after > Math.Min(SlotWalk.Unbounded, walk.ReachRight + taken))
+ return false;
+
+ FeatureStruct required = sequence[sequence.Length - 1 - taken];
+ if (required == null || IsUnifiable(node, required, ref unifications))
{
- found = IsPresent(summary.Distinct[index], shape, ref unifications);
- known |= bit;
- if (found)
- present |= bit;
+ suffixStart = ordinal;
+ taken++;
}
- if (!found)
+ }
+ if (segment && !node.Annotation.Optional)
+ after++;
+ }
+ return taken == allomorph.Post;
+ }
+
+ // Once a slot that can reorder nodes has been walked the order of the shape says nothing about the order of
+ // the shape the slot unapplies to, so each constraint is only looked for on its own.
+ private static bool AllPresent(AllomorphSummary allomorph, Shape shape, ref long unifications)
+ {
+ foreach (FeatureStruct required in allomorph.Sequence)
+ {
+ if (required == null)
+ continue;
+
+ bool found = false;
+ foreach (ShapeNode node in shape)
+ {
+ if (node.Type() == HCFeatureSystem.Segment && IsUnifiable(node, required, ref unifications))
{
- all = false;
+ found = true;
break;
}
}
- if (all)
+ if (!found)
return false;
}
return true;
}
- private static bool IsPresent(FeatureStruct required, Shape shape, ref long unifications)
+ private static bool IsUnifiable(ShapeNode node, FeatureStruct required, ref long unifications)
{
- foreach (ShapeNode node in shape)
- {
- if (node.Type() != HCFeatureSystem.Segment)
- continue;
-
- unifications++;
- if (node.Annotation.FeatureStruct.IsUnifiable(required, false, null))
- return true;
- }
- return false;
+ unifications++;
+ return node.Annotation.FeatureStruct.IsUnifiable(required, false, null);
}
private static SlotSummary Summarize(AffixTemplateSlot slot)
{
var summary = new SlotSummary();
- var allomorphs = new List();
+ var allomorphs = new List();
foreach (MorphemicMorphologicalRule rule in slot.Rules)
{
IList ruleAllomorphs;
@@ -139,73 +301,117 @@ private static SlotSummary Summarize(AffixTemplateSlot slot)
default:
summary.MayAlwaysUnapply = true;
summary.IsPure = false;
+ summary.IsOrdered = false;
+ summary.ReachLeft = SlotWalk.Unbounded;
+ summary.ReachRight = SlotWalk.Unbounded;
continue;
}
foreach (AffixProcessAllomorph allomorph in ruleAllomorphs)
{
- if (!IsPure(allomorph))
+ bool pure = IsPure(allomorph);
+ if (!pure)
summary.IsPure = false;
+ if (!pure || !IsOrdered(allomorph))
+ summary.IsOrdered = false;
- FeatureStruct[] requirements = GetRequirements(allomorph);
- if (requirements.Length == 0)
+ AllomorphSummary allomorphSummary = Summarize(allomorph);
+ summary.ReachLeft = Math.Max(summary.ReachLeft, allomorphSummary.ReachLeft);
+ summary.ReachRight = Math.Max(summary.ReachRight, allomorphSummary.ReachRight);
+ if (allomorphSummary.Sequence.Length == 0)
summary.MayAlwaysUnapply = true;
else
- allomorphs.Add(requirements);
+ allomorphs.Add(allomorphSummary);
}
}
- var distinct = new List();
- summary.Allomorphs = allomorphs
- .Select(requirements =>
- requirements
- .Select(fs =>
- {
- int index = distinct.FindIndex(d => d.ValueEquals(fs));
- if (index < 0)
- {
- distinct.Add(fs);
- index = distinct.Count - 1;
- }
- return index;
- })
- .ToArray()
- )
- .ToArray();
- summary.Distinct = distinct.ToArray();
+ summary.Allomorphs = allomorphs.ToArray();
return summary;
}
- private static FeatureStruct[] GetRequirements(AffixProcessAllomorph allomorph)
+ // Pre is the inserts before the first non-insertion, Post those after the last; any between is an infix.
+ // A non-segment constraint matches nothing the matcher sees, so skipping it only weakens the test.
+ private static AllomorphSummary Summarize(AffixProcessAllomorph allomorph)
{
- var requirements = new List();
- foreach (MorphologicalOutputAction action in allomorph.Rhs)
+ var sequence = new List();
+ IList rhs = allomorph.Rhs;
+ int first = -1;
+ int last = -1;
+ for (int i = 0; i < rhs.Count; i++)
{
- if (action is InsertSegments insertSegments)
+ if (IsInsertion(rhs[i]))
+ continue;
+ if (first < 0)
+ first = i;
+ last = i;
+ }
+
+ int pre = 0;
+ int post = 0;
+ bool infix = false;
+ for (int i = 0; i < rhs.Count; i++)
+ {
+ if (!IsInsertion(rhs[i]))
+ continue;
+
+ int before = sequence.Count;
+ foreach (FeatureStruct fs in InsertedConstraints(rhs[i]))
{
- foreach (ShapeNode node in insertSegments.Segments.Shape)
+ if ((FeatureSymbol)fs.GetValue(HCFeatureSystem.Type) != HCFeatureSystem.Segment)
+ continue;
+
+ // A constraint with variables is bound by earlier matches, so any segment stands for it.
+ FeatureStruct copy = null;
+ if (!fs.HasVariables)
{
- if (node.Type() != HCFeatureSystem.Boundary)
- AddRequirement(requirements, node.Annotation.FeatureStruct);
+ copy = fs.Clone();
+ copy.Freeze();
}
+ sequence.Add(copy);
}
- else if (action is InsertSimpleContext insertContext)
- {
- AddRequirement(requirements, insertContext.SimpleContext.FeatureStruct);
- }
+ int added = sequence.Count - before;
+ if (first < 0 || (i > first && i < last))
+ infix = true;
+ else if (i < first)
+ pre += added;
+ else
+ post += added;
+ }
+
+ var summary = new AllomorphSummary { Sequence = sequence.ToArray() };
+ if (infix)
+ {
+ summary.ReachLeft = SlotWalk.Unbounded;
+ summary.ReachRight = SlotWalk.Unbounded;
}
- return requirements.ToArray();
+ else
+ {
+ summary.Pre = pre;
+ summary.Post = post;
+ summary.ReachLeft = pre;
+ summary.ReachRight = post;
+ }
+ return summary;
}
- // A constraint with variables is bound by earlier matches, and one that is not a segment is never
- // matched against the segments the matcher considers, so neither can be tested here.
- private static void AddRequirement(List requirements, FeatureStruct fs)
+ private static bool IsInsertion(MorphologicalOutputAction action)
{
- if (fs.HasVariables || (FeatureSymbol)fs.GetValue(HCFeatureSystem.Type) != HCFeatureSystem.Segment)
- return;
+ return action is InsertSegments || action is InsertSimpleContext;
+ }
- FeatureStruct copy = fs.Clone();
- copy.Freeze();
- requirements.Add(copy);
+ private static IEnumerable InsertedConstraints(MorphologicalOutputAction action)
+ {
+ if (action is InsertSegments insertSegments)
+ {
+ foreach (ShapeNode node in insertSegments.Segments.Shape)
+ {
+ if (node.Type() != HCFeatureSystem.Boundary)
+ yield return node.Annotation.FeatureStruct;
+ }
+ }
+ else if (action is InsertSimpleContext insertContext)
+ {
+ yield return insertContext.SimpleContext.FeatureStruct;
+ }
}
private static bool IsPure(AffixProcessAllomorph allomorph)
@@ -215,7 +421,7 @@ private static bool IsPure(AffixProcessAllomorph allomorph)
{
if (action is CopyFromInput)
copied.Add(action.PartName);
- else if (!(action is InsertSegments || action is InsertSimpleContext))
+ else if (!IsInsertion(action))
return false;
}
@@ -229,6 +435,24 @@ private static bool IsPure(AffixProcessAllomorph allomorph)
return true;
}
+ // GenerateShape writes parts in left-hand order, so copying them in another order, or one twice, can
+ // reorder or repeat nodes.
+ private static bool IsOrdered(AffixProcessAllomorph allomorph)
+ {
+ int previous = -1;
+ foreach (MorphologicalOutputAction action in allomorph.Rhs)
+ {
+ if (!(action is CopyFromInput))
+ continue;
+
+ int index = allomorph.Lhs.ToList().FindIndex(part => part.Name == action.PartName);
+ if (index <= previous)
+ return false;
+ previous = index;
+ }
+ return true;
+ }
+
private static bool HasSegmentConstraint(PatternNode node)
{
if (node is Constraint constraint)
@@ -253,12 +477,23 @@ private static int MinSegments(PatternNode node)
}
}
+ private class AllomorphSummary
+ {
+ public FeatureStruct[] Sequence;
+ public int Pre;
+ public int Post;
+ public int ReachLeft;
+ public int ReachRight;
+ }
+
private class SlotSummary
{
public bool IsPure = true;
+ public bool IsOrdered = true;
public bool MayAlwaysUnapply;
- public int[][] Allomorphs;
- public FeatureStruct[] Distinct;
+ public int ReachLeft;
+ public int ReachRight;
+ public AllomorphSummary[] Allomorphs;
}
}
}
diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/AnalysisAffixTemplateOrderTests.cs b/tests/SIL.Machine.Morphology.HermitCrab.Tests/AnalysisAffixTemplateOrderTests.cs
new file mode 100644
index 000000000..93b223e20
--- /dev/null
+++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/AnalysisAffixTemplateOrderTests.cs
@@ -0,0 +1,362 @@
+using NUnit.Framework;
+using SIL.Machine.Annotations;
+using SIL.Machine.FeatureModel;
+using SIL.Machine.Matching;
+using SIL.Machine.Morphology.HermitCrab.MorphologicalRules;
+
+namespace SIL.Machine.Morphology.HermitCrab;
+
+// Order and edge tests of the required-slot look-ahead; slots are in synthesis order and unapplied last to first.
+[TestFixture]
+public class AnalysisAffixTemplateOrderTests : HermitCrabTestBase
+{
+ private static readonly FeatureStruct Any = FeatureStruct.New().Symbol(HCFeatureSystem.Segment).Value;
+
+ private sealed record WalkResult(List Parses, int RuleAttempts, int Violations);
+
+ private static AffixTemplateSlot Slot(bool optional, params MorphemicMorphologicalRule[] rules) =>
+ new(rules) { Optional = optional };
+
+ private static AffixTemplate TemplateOf(params AffixTemplateSlot[] slots)
+ {
+ var template = new AffixTemplate { Name = "order_template" };
+ foreach (AffixTemplateSlot slot in slots)
+ template.Slots.Add(slot);
+ return template;
+ }
+
+ private static Pattern Part(string name) =>
+ Pattern.New(name).Annotation(Any).OneOrMore.Value;
+
+ private static Pattern OneSegment(string name) =>
+ Pattern.New(name).Annotation(Any).Value;
+
+ private static AffixProcessRule RuleOf(
+ string name,
+ Pattern[] lhs,
+ params MorphologicalOutputAction[] rhs
+ )
+ {
+ var rule = new AffixProcessRule
+ {
+ Id = name,
+ Name = name,
+ Gloss = name,
+ RequiredSyntacticFeatureStruct = FeatureStruct.New().Value,
+ OutSyntacticFeatureStruct = FeatureStruct.New().Value,
+ };
+ var allomorph = new AffixProcessAllomorph();
+ foreach (Pattern part in lhs)
+ allomorph.Lhs.Add(part);
+ foreach (MorphologicalOutputAction action in rhs)
+ allomorph.Rhs.Add(action);
+ rule.Allomorphs.Add(allomorph);
+ return rule;
+ }
+
+ private AffixProcessRule Suffix(string insert) =>
+ RuleOf("suffix_" + insert, new[] { Part("1") }, new CopyFromInput("1"), new InsertSegments(Table3, insert));
+
+ private AffixProcessRule Prefix(string insert) =>
+ RuleOf("prefix_" + insert, new[] { Part("1") }, new InsertSegments(Table3, insert), new CopyFromInput("1"));
+
+ private AffixProcessRule Circumfix(string before, string after) =>
+ RuleOf(
+ "circumfix_" + before + after,
+ new[] { Part("1") },
+ new InsertSegments(Table3, before),
+ new CopyFromInput("1"),
+ new InsertSegments(Table3, after)
+ );
+
+ // inserts the material after the first segment of the stem
+ private AffixProcessRule Infix(string insert) =>
+ RuleOf(
+ "infix_" + insert,
+ new[] { OneSegment("1"), Part("2") },
+ new CopyFromInput("1"),
+ new InsertSegments(Table3, insert),
+ new CopyFromInput("2")
+ );
+
+ // writes the last segment of the stem twice
+ private static AffixProcessRule RepeatLast() =>
+ RuleOf(
+ "repeat_last",
+ new[] { Part("1"), OneSegment("2") },
+ new CopyFromInput("1"),
+ new CopyFromInput("2"),
+ new CopyFromInput("2")
+ );
+
+ // swaps the last two segments of the stem, so it copies its parts out of order
+ private static AffixProcessRule Metathesis() =>
+ RuleOf(
+ "metathesis",
+ new[] { Part("1"), OneSegment("2"), OneSegment("3") },
+ new CopyFromInput("1"),
+ new CopyFromInput("3"),
+ new CopyFromInput("2")
+ );
+
+ private WalkResult Walk(
+ AffixTemplate template,
+ string form,
+ RequiredSlotLookaheadMode mode,
+ int maxDegreeOfParallelism = 1
+ )
+ {
+ int attempts = 0;
+ int violations = 0;
+ var morpher = new Morpher(TraceManager, Language, maxDegreeOfParallelism)
+ {
+ RequiredSlotLookahead = mode,
+ RuleSelector = rule =>
+ {
+ if (rule is AffixProcessRule)
+ System.Threading.Interlocked.Increment(ref attempts);
+ return true;
+ },
+ RequiredSlotShadow = (kind, t, slot) =>
+ {
+ if (kind != "fired")
+ System.Threading.Interlocked.Increment(ref violations);
+ },
+ };
+ TraceManager.IsTracing = false;
+ var input = new Word(Morphophonemic, Morphophonemic.CharacterDefinitionTable.Segment(form));
+ input.Freeze();
+ List parses = template
+ .CompileAnalysisRule(morpher)
+ .Apply(input)
+ .Select(w => w.ToString() + " " + string.Join(",", w.AllomorphsInMorphOrder.Select(a => a.Morpheme.Gloss)))
+ .OrderBy(p => p, System.StringComparer.Ordinal)
+ .ToList();
+ return new WalkResult(parses, attempts, violations);
+ }
+
+ private (WalkResult Off, WalkResult On) WalkBoth(AffixTemplate template, string form)
+ {
+ WalkResult off = Walk(template, form, RequiredSlotLookaheadMode.Off);
+ WalkResult on = Walk(template, form, RequiredSlotLookaheadMode.On);
+ Assert.That(on.Parses, Is.EqualTo(off.Parses), "the look-ahead must not change the parses of " + form);
+ return (off, on);
+ }
+
+ [Test]
+ public void Apply_RequiredSuffixesInTheWrongOrder_PrunesAtEntry()
+ {
+ // both literals are present in "saguti", but the outer suffix -u has to end the word
+ AffixTemplate template = TemplateOf(Slot(false, Suffix("ti")), Slot(false, Suffix("u")));
+
+ (WalkResult off, WalkResult wrong) = WalkBoth(template, "saguti");
+ (WalkResult offRight, WalkResult right) = WalkBoth(template, "sagtiu");
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(off.Parses, Is.Empty);
+ Assert.That(off.RuleAttempts, Is.GreaterThan(0));
+ Assert.That(wrong.RuleAttempts, Is.EqualTo(0));
+ Assert.That(right.Parses, Is.Not.Empty);
+ Assert.That(right.RuleAttempts, Is.EqualTo(offRight.RuleAttempts));
+ });
+ }
+
+ [Test]
+ public void Apply_PrefixSlotWhoseMaterialIsNotAtTheLeftEdge_PrunesAtEntry()
+ {
+ AffixTemplate template = TemplateOf(Slot(false, Suffix("ti")), Slot(false, Prefix("ka")));
+
+ (WalkResult off, WalkResult wrong) = WalkBoth(template, "sagkati");
+ (WalkResult offRight, WalkResult right) = WalkBoth(template, "kasagti");
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(off.RuleAttempts, Is.GreaterThan(0));
+ Assert.That(wrong.RuleAttempts, Is.EqualTo(0));
+ Assert.That(right.Parses, Is.Not.Empty);
+ Assert.That(right.RuleAttempts, Is.EqualTo(offRight.RuleAttempts));
+ });
+ }
+
+ [Test]
+ public void Apply_CircumfixWhoseSuffixPartIsNotAtTheRightEdge_PrunesAtEntry()
+ {
+ AffixTemplate template = TemplateOf(Slot(false, Circumfix("ka", "ti")));
+
+ (WalkResult off, WalkResult wrong) = WalkBoth(template, "kasagtz");
+ (WalkResult offRight, WalkResult right) = WalkBoth(template, "kasagti");
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(wrong.RuleAttempts, Is.EqualTo(0));
+ Assert.That(off.RuleAttempts, Is.GreaterThan(0));
+ Assert.That(right.Parses, Is.Not.Empty);
+ Assert.That(right.RuleAttempts, Is.EqualTo(offRight.RuleAttempts));
+ });
+ }
+
+ [Test]
+ public void Apply_CircumfixPartsThatWouldShareANode_PrunesAtEntry()
+ {
+ // ka- and -ak could both use the a of "kak"
+ AffixTemplate template = TemplateOf(Slot(false, Circumfix("ka", "ak")));
+
+ (WalkResult off, WalkResult on) = WalkBoth(template, "kak");
+
+ Assert.That(on.RuleAttempts, Is.EqualTo(0));
+ Assert.That(off.RuleAttempts, Is.GreaterThan(0));
+ }
+
+ // an infix does not anchor, so only the window left by the outer required prefix can rule out its material
+ private AffixTemplate InfixInsidePrefixTemplate() =>
+ TemplateOf(Slot(false, Infix("ak")), Slot(false, Prefix("ka")));
+
+ [Test]
+ public void Apply_InnerMaterialOnlyInsideTheOuterPrefix_PrunesAtEntry()
+ {
+ // the a of "ka" is the only a before the k that -ak needs, so the infix has no material of its own
+ (WalkResult off, WalkResult on) = WalkBoth(InfixInsidePrefixTemplate(), "kakg");
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(off.RuleAttempts, Is.GreaterThan(0));
+ Assert.That(on.RuleAttempts, Is.EqualTo(0));
+ });
+ }
+
+ [Test]
+ public void Apply_InnerMaterialAfterTheOuterPrefix_IsNotPruned()
+ {
+ // "sag" with the infix ak is "sakag", and ka- in front gives "kasakag"
+ (WalkResult off, WalkResult on) = WalkBoth(InfixInsidePrefixTemplate(), "kasakag");
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(on.Parses, Is.Not.Empty);
+ Assert.That(on.RuleAttempts, Is.EqualTo(off.RuleAttempts));
+ });
+ }
+
+ [Test]
+ public void Apply_OptionalOuterPrefix_CountsTowardsTheLeftEdgeBudget()
+ {
+ AffixTemplate template = TemplateOf(Slot(false, Prefix("ka")), Slot(true, Prefix("ba")));
+
+ (WalkResult off, WalkResult on) = WalkBoth(template, "bakasag");
+
+ Assert.That(on.Parses, Is.Not.Empty);
+ }
+
+ [Test]
+ public void Apply_OuterSlotWithNullAffix_IsNeverPrunedAndTakesNoEdgeMaterial()
+ {
+ AffixProcessRule uOrNull = Suffix("u");
+ uOrNull.Allomorphs.Add(new AffixProcessAllomorph { Lhs = { Part("1") }, Rhs = { new CopyFromInput("1") } });
+ AffixTemplate template = TemplateOf(Slot(false, Suffix("ti")), Slot(false, uOrNull));
+
+ (WalkResult off, WalkResult on) = WalkBoth(template, "sagti");
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(on.Parses, Is.Not.Empty);
+ Assert.That(on.RuleAttempts, Is.EqualTo(off.RuleAttempts));
+ });
+ }
+
+ [Test]
+ public void Apply_OptionalOuterSlot_CountsTowardsTheEdgeBudget()
+ {
+ // the optional -z may stand between -ti and the end of the word
+ AffixTemplate template = TemplateOf(Slot(false, Suffix("ti")), Slot(true, Suffix("z")));
+
+ (WalkResult off, WalkResult on) = WalkBoth(template, "sagtiz");
+
+ Assert.That(on.Parses, Is.Not.Empty);
+ }
+
+ [Test]
+ public void Apply_InfixSlotBetween_DoesNotAnchorTheInnerSlot()
+ {
+ // unapplying the infix z of "kzasag" gives "kasag", which starts with ka-
+ AffixTemplate template = TemplateOf(Slot(false, Prefix("ka")), Slot(true, Infix("z")));
+
+ (WalkResult off, WalkResult on) = WalkBoth(template, "kzasag");
+
+ Assert.That(on.Parses, Is.Not.Empty);
+ }
+
+ [Test]
+ public void Apply_SlotThatCopiesOutOfOrderBetween_ChecksOnlyPresence()
+ {
+ // unapplying the metathesis of "sagit" gives "sagti", whose -ti is in the opposite order
+ AffixTemplate template = TemplateOf(Slot(false, Suffix("ti")), Slot(true, Metathesis()));
+
+ (WalkResult off, WalkResult on) = WalkBoth(template, "sagit");
+
+ Assert.That(on.Parses, Is.Not.Empty);
+ }
+
+ [Test]
+ public void Apply_PartCopiedTwiceBetween_DoesNotAnchorTheInnerSlot()
+ {
+ // unapplying the repeated i of "sagtii" gives "sagti", which ends in -ti
+ AffixTemplate template = TemplateOf(Slot(false, Suffix("ti")), Slot(true, RepeatLast()));
+
+ (WalkResult off, WalkResult on) = WalkBoth(template, "sagtii");
+
+ Assert.That(on.Parses, Is.Not.Empty);
+ }
+
+ [Test]
+ public void Apply_ConstraintWithVariables_IsMatchedByAnySegment()
+ {
+ var context = new SimpleContext(new NaturalClass(Any), new[] { Variable("vd", "a") });
+ AffixProcessRule suffix = RuleOf(
+ "variable_suffix",
+ new[] { Part("1") },
+ new CopyFromInput("1"),
+ new InsertSimpleContext(context)
+ );
+ AffixTemplate template = TemplateOf(Slot(false, suffix));
+
+ foreach (string form in new[] { "sagd", "sagt", "sag" })
+ WalkBoth(template, form);
+ }
+
+ [TestCase(1)]
+ [TestCase(0)]
+ public void Apply_OnEveryTemplateAndWord_KeepsTheParsesAndNeverViolatesTheShadow(int maxDegreeOfParallelism)
+ {
+ var templates = new Dictionary
+ {
+ [TemplateOf(Slot(false, Suffix("ti")), Slot(false, Suffix("u")))] = new[] { "saguti", "sagtiu", "sagti" },
+ [TemplateOf(Slot(false, Suffix("ti")), Slot(false, Prefix("ka")))] = new[] { "kasagti", "sagkati" },
+ [TemplateOf(Slot(false, Circumfix("ka", "ti")), Slot(true, Suffix("z")), Slot(false, Prefix("ba")))] = new[]
+ {
+ "bakasagtiz",
+ "kabasagti",
+ "bakasagtzi",
+ },
+ [TemplateOf(Slot(false, Prefix("ka")), Slot(true, Infix("z")))] = new[] { "kzasag", "kasag" },
+ [TemplateOf(Slot(false, Suffix("ti")), Slot(true, RepeatLast()))] = new[] { "sagtii", "sagti" },
+ [TemplateOf(Slot(false, Suffix("ti")), Slot(true, Metathesis()))] = new[] { "sagit", "sagti" },
+ };
+
+ foreach ((AffixTemplate template, string[] forms) in templates)
+ {
+ foreach (string form in forms)
+ {
+ WalkResult off = Walk(template, form, RequiredSlotLookaheadMode.Off, maxDegreeOfParallelism);
+ WalkResult on = Walk(template, form, RequiredSlotLookaheadMode.On, maxDegreeOfParallelism);
+ WalkResult shadow = Walk(template, form, RequiredSlotLookaheadMode.Shadow, maxDegreeOfParallelism);
+ Assert.Multiple(() =>
+ {
+ Assert.That(on.Parses, Is.EqualTo(off.Parses), form);
+ Assert.That(shadow.Parses, Is.EqualTo(off.Parses), form);
+ Assert.That(shadow.Violations, Is.EqualTo(0), form);
+ });
+ }
+ }
+ }
+}