Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ public IEnumerable<Word> Apply(Word input)
return output;
}

// Testing runs from the next slot to unapply and stops after an impure slot, which makes the test
// for any slot behind it unsound. Shadow mode prunes nothing and carries the fired slots in fired.
// Testing runs from the next slot to unapply and stops after a slot whose written-back segments cannot be
// bounded. Shadow mode prunes nothing and carries the fired slots in fired.
private bool RequiredSlotCannotUnapply(Word word, int lastSlot, ref int[] fired)
{
RequiredSlotLookaheadMode mode = _morpher.RequiredSlotLookahead;
Expand All @@ -111,7 +111,7 @@ private bool RequiredSlotCannotUnapply(Word word, int lastSlot, ref int[] fired)
_morpher.RequiredSlotShadow?.Invoke("fired", _template, j);
}
_lookahead.Advance(j, ref walk);
if (!_lookahead.IsPure(j))
if (!_lookahead.CanWalkPast(j))
break;
}
if (unifications != 0)
Expand Down
138 changes: 126 additions & 12 deletions src/SIL.Machine.Morphology.HermitCrab/RequiredSlotMaterial.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ internal struct SlotWalk
/// <summary>False once a slot walked so far can reorder the nodes it copies.</summary>
public bool Ordered;

/// <summary>
/// The slots walked so far that can write segments the shape does not hold, as a bit mask of slot indexes.
/// </summary>
public int Suppliers;

public static SlotWalk Start()
{
return new SlotWalk
Expand Down Expand Up @@ -94,20 +99,28 @@ public static SlotWalk Start()
/// </remarks>
internal class RequiredSlotMaterial
{
// Slots at or above this index cannot be recorded in SlotWalk.Suppliers.
private const int MaxSupplierSlots = 31;

private readonly SlotSummary[] _slots;

public RequiredSlotMaterial(AffixTemplate template)
{
_slots = template.Slots.Select(Summarize).ToArray();
for (int i = 0; i < _slots.Length; i++)
{
foreach (AllomorphSummary allomorph in _slots[i].Allomorphs)
allomorph.Cover = CoverMasks(allomorph, i);
}
}

/// <summary>
/// Whether every rule of the slot is pure, so that unapplying the slot leaves the segments needed by
/// slots before it no easier to find.
/// Whether the walk may go on past the slot: it is pure, or the segments it can write are bounded, so that
/// the slots before it are tested with those segments allowed to be missing from the shape.
/// </summary>
public bool IsPure(int slot)
public bool CanWalkPast(int slot)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

FYI: F1. The class remarks still give the old soundness argument, which this change makes false: RequiredSlotMaterial.cs:82-87 says GenerateShape "never untruncates a segment" and that the test for slot j "is used only when every rule in the slots still to be walked after j is pure". CanWalkPast now walks past suppliers. Add a Supply paragraph (budget, cover, why suppliers force the unordered test) so the next change to Embeds does not rely on the old invariant.

{
return _slots[slot].IsPure;
return _slots[slot].IsPure || (_slots[slot].Suppliable && slot < MaxSupplierSlots);
}

/// <summary>
Expand All @@ -117,6 +130,8 @@ public bool IsPure(int slot)
public void Advance(int slot, ref SlotWalk walk)
{
SlotSummary summary = _slots[slot];
if (!summary.IsPure && summary.Suppliable && slot < MaxSupplierSlots)
walk.Suppliers |= 1 << slot;
if (!walk.Ordered || !summary.IsOrdered)
{
walk.Ordered = false;
Expand All @@ -132,7 +147,8 @@ public void Advance(int slot, ref SlotWalk walk)

/// <summary>
/// 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. When the slot can unapply and is ordered, narrows the
/// walked between this word and the slot is pure or has its written-back segments bounded and counted
/// in the walk. When the slot can unapply and is ordered, narrows the
/// window of the walk for the slots before it.
/// </summary>
/// <param name="slot">The slot to test.</param>
Expand All @@ -157,7 +173,7 @@ public bool CannotUnapply(int slot, Shape shape, ref SlotWalk walk, ref long uni
int suffixStart = int.MaxValue;
bool found = walk.Ordered
? Embeds(allomorph, shape, ref walk, ref unifications, out prefixEnd, out suffixStart)
: AllPresent(allomorph, shape, ref unifications);
: AllPresent(allomorph, shape, walk.Suppliers, ref unifications);
if (!found)
continue;

Expand Down Expand Up @@ -254,12 +270,15 @@ out int suffixStart
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)
// The order of the shape says nothing once a reordering slot is walked, so each constraint is looked for
// alone. One with no node may be met by a segment a walked supplier writes, once each.
private bool AllPresent(AllomorphSummary allomorph, Shape shape, int suppliers, ref long unifications)
{
foreach (FeatureStruct required in allomorph.Sequence)
int missing = 0;
int covering = 0;
for (int i = 0; i < allomorph.Sequence.Length; i++)
{
FeatureStruct required = allomorph.Sequence[i];
if (required == null)
continue;

Expand All @@ -272,10 +291,46 @@ private static bool AllPresent(AllomorphSummary allomorph, Shape shape, ref long
break;
}
}
if (!found)
if (found)
continue;

int coverable = allomorph.Cover[i] & suppliers;
if (coverable == 0)
return false;
missing++;
covering |= coverable;
}
return true;
if (missing == 0)
return true;

int budget = 0;
for (int slot = 0; covering != 0; slot++, covering >>= 1)
{
if ((covering & 1) != 0)
budget += _slots[slot].Budget;
}
return missing <= budget;
}

// For each constraint, the slots that can write a segment it unifies with.
private int[] CoverMasks(AllomorphSummary allomorph, int own)
{
var masks = new int[allomorph.Sequence.Length];
for (int i = 0; i < masks.Length; i++)
{
FeatureStruct required = allomorph.Sequence[i];
if (required == null)
continue;

for (int slot = 0; slot < _slots.Length && slot < MaxSupplierSlots; slot++)
{
if (slot == own || !_slots[slot].Suppliable || _slots[slot].IsPure)
continue;
if (_slots[slot].Supply.Any(fs => fs == null || fs.IsUnifiable(required, false, null)))
masks[i] |= 1 << slot;
}
}
return masks;
}

private static bool IsUnifiable(ShapeNode node, FeatureStruct required, ref long unifications)
Expand All @@ -288,6 +343,7 @@ private static SlotSummary Summarize(AffixTemplateSlot slot)
{
var summary = new SlotSummary();
var allomorphs = new List<AllomorphSummary>();
var supply = new List<FeatureStruct>();
foreach (MorphemicMorphologicalRule rule in slot.Rules)
{
IList<AffixProcessAllomorph> ruleAllomorphs;
Expand All @@ -302,6 +358,7 @@ private static SlotSummary Summarize(AffixTemplateSlot slot)
default:
summary.MayAlwaysUnapply = true;
summary.IsPure = false;
summary.Suppliable = false;
summary.IsOrdered = false;
summary.ReachLeft = SlotWalk.Unbounded;
summary.ReachRight = SlotWalk.Unbounded;
Expand All @@ -313,6 +370,11 @@ private static SlotSummary Summarize(AffixTemplateSlot slot)
bool pure = IsPure(allomorph);
if (!pure)
summary.IsPure = false;
int restored = Restored(allomorph, supply);
if (restored < 0)
summary.Suppliable = false;
else
summary.Budget = Math.Max(summary.Budget, restored);
if (!pure || !IsOrdered(allomorph))
summary.IsOrdered = false;

Expand All @@ -326,6 +388,7 @@ private static SlotSummary Summarize(AffixTemplateSlot slot)
}
}
summary.Allomorphs = allomorphs.ToArray();
summary.Supply = supply.ToArray();
return summary;
}

Expand Down Expand Up @@ -438,6 +501,53 @@ private static bool IsPure(AffixProcessAllomorph allomorph)
return true;
}

// The most segments the allomorph writes back, their constraints added to supply (null with variables).
// -1 if it does more than copy and insert, as a modified node can change any feature of any node.
private static int Restored(AffixProcessAllomorph allomorph, List<FeatureStruct> supply)
{
var copied = new HashSet<string>();
foreach (MorphologicalOutputAction action in allomorph.Rhs)
{
if (action is CopyFromInput)
copied.Add(action.PartName);
else if (!IsInsertion(action))
return -1;
}

int total = 0;
foreach (Pattern<Word, ShapeNode> part in allomorph.Lhs)
{
if (!HasSegmentConstraint(part) || RestoredSegments(part) == 0)
continue;
if (copied.Contains(part.Name) && MinSegments(part) > 0)
continue;
total += RestoredSegments(part);
AddRestoredConstraints(part, supply);
}
return total;
}

private static void AddRestoredConstraints(PatternNode<Word, ShapeNode> node, List<FeatureStruct> supply)
{
foreach (PatternNode<Word, ShapeNode> child in node.Children)
{
if (child is Constraint<Word, ShapeNode> constraint && constraint.Type() == HCFeatureSystem.Segment)
{
FeatureStruct copy = null;
if (!constraint.FeatureStruct.HasVariables)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

FYI: F3. Nothing tests the variable branch: a deleted constraint with variables adds a null supply, which CoverMasks treats as covering any constraint. I searched AnalysisAffixTemplateSupplyTests.cs for "Variable" and "alpha" and found nothing. If null were treated as covering nothing, alpha-feature truncations would be pruned and no test would fail. Add one truncating slot whose deleted segment carries a variable.

{
copy = constraint.FeatureStruct.Clone();
copy.Freeze();
}
supply.Add(copy);
}
else if (!(child is Quantifier<Word, ShapeNode> quantifier) || quantifier.MaxOccur > 0)
{
AddRestoredConstraints(child, supply);
}
}
}

// 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)
Expand Down Expand Up @@ -500,6 +610,7 @@ private static int MinSegments(PatternNode<Word, ShapeNode> node)
private class AllomorphSummary
{
public FeatureStruct[] Sequence;
public int[] Cover;
public int Pre;
public int Post;
public int ReachLeft;
Expand All @@ -511,6 +622,9 @@ private class SlotSummary
public bool IsPure = true;
public bool IsOrdered = true;
public bool MayAlwaysUnapply;
public bool Suppliable = true;
public int Budget;
public FeatureStruct[] Supply;
public int ReachLeft;
public int ReachRight;
public AllomorphSummary[] Allomorphs;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,9 @@ public void Apply_SlotWithABoundedRunThatMayBeEmpty_RestoresASegmentTheInnerSlot
}

[Test]
public void Apply_SlotWithABoundedRunThatMayBeEmpty_StaysImpureWhenTheMaterialIsAbsent()
public void Apply_SlotWithABoundedRunThatMayBeEmpty_StaysImpureWhenTheRunCouldBeTheMissingSegment()
{
AffixTemplate template = TemplateOf(Slot(false, Suffix("ti")), Slot(true, BoundedRunSuffix("z")));
AffixTemplate template = TemplateOf(Slot(false, Suffix("i")), Slot(true, BoundedRunSuffix("z")));

(WalkResult off, WalkResult on) = WalkBoth(template, "sagz");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,11 +280,12 @@ public void Apply_RequiredSlotWithNullAffix_IsNeverPruned()
});
}

// An optional slot whose rule inserts "z" after the copied parts; the word never contains the t and i
// that the required inner slot inserts, so a pure rule lets the walk prune at entry, before the optional rule is tried, and an impure one does not.
// An optional slot inserting "z"; the word lacks the inner slot's segments, so a pure rule prunes at entry.
// An impure rule does not if it can restore them: those cases use the one segment it may restore.
private (WalkResult Off, WalkResult On) WalkWithOptionalSlot(
Pattern<Word, ShapeNode>[] lhs,
MorphologicalOutputAction[] rhs
MorphologicalOutputAction[] rhs,
string inner = "ti"
)
{
var rule = new AffixProcessRule
Expand All @@ -302,7 +303,7 @@ MorphologicalOutputAction[] rhs
allomorph.Rhs.Add(action);
allomorph.Rhs.Add(new InsertSegments(Table3, "z"));
rule.Allomorphs.Add(allomorph);
AffixTemplate template = TemplateOf(Slot(false, SuffixRule("ti", "ti")), Slot(true, rule));
AffixTemplate template = TemplateOf(Slot(false, SuffixRule(inner, inner)), Slot(true, rule));
return WalkBoth(template, "sagz");
}

Expand All @@ -311,7 +312,8 @@ public void Apply_CopiedPartCappedAtOneThatMayMatchNothing_MakesTheSlotImpure()
{
(WalkResult off, WalkResult on) = WalkWithOptionalSlot(
new[] { Pattern<Word, ShapeNode>.New("1").Annotation(Any).Optional.Value },
new MorphologicalOutputAction[] { new CopyFromInput("1") }
new MorphologicalOutputAction[] { new CopyFromInput("1") },
"i"
);

Assert.That(on.RuleAttempts, Is.GreaterThan(0), "the optional rule is still tried at entry");
Expand Down Expand Up @@ -370,7 +372,8 @@ public void Apply_CopiedAlternationWithAnOptionalBranch_MakesTheSlotImpure()
.Optional.Or.Annotation(Character(Table3, "u"))
.Value,
},
new MorphologicalOutputAction[] { new CopyFromInput("1"), new CopyFromInput("2") }
new MorphologicalOutputAction[] { new CopyFromInput("1"), new CopyFromInput("2") },
"i"
);

Assert.That(on.RuleAttempts, Is.GreaterThan(0), "the optional rule is still tried at entry");
Expand Down
Loading
Loading