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
114 changes: 103 additions & 11 deletions src/SIL.Machine.Morphology.HermitCrab/AnalysisAffixTemplateRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ internal class AnalysisAffixTemplateRule : IRule<Word, ShapeNode>
private readonly Morpher _morpher;
private readonly AffixTemplate _template;
private readonly List<IRule<Word, ShapeNode>> _rules;
private readonly RequiredSlotMaterial _lookahead;

public AnalysisAffixTemplateRule(Morpher morpher, AffixTemplate template)
{
Expand All @@ -26,6 +27,7 @@ public AnalysisAffixTemplateRule(Morpher morpher, AffixTemplate template)
FreezableEqualityComparer<Word>.Default
))
);
_lookahead = new RequiredSlotMaterial(template);
}

public IEnumerable<Word> Apply(Word input)
Expand Down Expand Up @@ -61,24 +63,96 @@ public IEnumerable<Word> Apply(Word input)
return Enumerable.Empty<Word>();
}

int[] fired = null;
if (RequiredSlotCannotUnapply(input, _rules.Count - 1, ref fired))
{
if (_morpher.TraceManager.IsTracing)
_morpher.TraceManager.EndUnapplyTemplate(_template, input, false, FailureReason.None);

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.

Minor: F1. Tracing loses the failure reason: with the prune on by default, a pruned template now traces as TemplateAnalysisInput then a null output with FailureReason.None, and the per-rule not-unapplied entries that showed which slot failed are gone. Apply_Tracing_ShowsAPrunedTemplateAsAFailedOutput locks this in for sagufz. Skip the prune while TraceManager.IsTracing, or give it its own FailureReason.

return Enumerable.Empty<Word>();
}

Word inWord = input.Clone();
inWord.Freeze();

var output = new HashSet<Word>(FreezableEqualityComparer<Word>.Default);
if (_morpher.MaxDegreeOfParallelism == 1)
ApplySlots(inWord, _rules.Count - 1, output);
ApplySlots(inWord, _rules.Count - 1, output, fired);
else
ParallelApplySlots(inWord, output);
ParallelApplySlots(inWord, output, fired);

return output;
}

private void ApplySlots(Word inWord, int index, HashSet<Word> 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.
private bool RequiredSlotCannotUnapply(Word word, int lastSlot, ref int[] fired)
{
RequiredSlotLookaheadMode mode = _morpher.RequiredSlotLookahead;
if (mode == RequiredSlotLookaheadMode.Off)
return false;

long unifications = 0;
bool cannot = false;
for (int j = lastSlot; j >= 0; j--)
{
if (
!_template.Slots[j].Optional
&& !IsFired(fired, j)
&& _lookahead.CannotUnapply(j, word.Shape, ref unifications)
)
{
if (mode == RequiredSlotLookaheadMode.On)
{
cannot = true;
break;
}
fired = fired == null ? new[] { j } : fired.Append(j).ToArray();
_morpher.RequiredSlotShadow?.Invoke("fired", _template, j);
}
if (!_lookahead.IsPure(j))
break;
}
if (unifications != 0)
_morpher.RequiredSlotCheckObserver?.Invoke(unifications);
return cannot;
}

private static bool IsFired(int[] fired, int slot)
{
return fired != null && Array.IndexOf(fired, slot) >= 0;
}

private void ReportShadow(string kind, int[] fired, int slot)
{
if (fired == null || _morpher.RequiredSlotShadow == null)
return;
if (slot < 0)
{
foreach (int j in fired)
_morpher.RequiredSlotShadow(kind, _template, j);
}
else if (IsFired(fired, slot))
{
_morpher.RequiredSlotShadow(kind, _template, slot);
}
}

private void ApplySlots(Word inWord, int index, HashSet<Word> output, int[] fired)
{
for (int i = index; i >= 0; i--)
{
foreach (Word outWord in _rules[i].Apply(inWord))
ApplySlots(outWord, i - 1, output);
{
ReportShadow("violated", fired, i);
int[] outFired = fired;
if (RequiredSlotCannotUnapply(outWord, i - 1, ref outFired))
{
if (_morpher.TraceManager.IsTracing)
_morpher.TraceManager.EndUnapplyTemplate(_template, outWord, false, FailureReason.None);
continue;
}
ApplySlots(outWord, i - 1, output, outFired);
}

if (!_template.Slots[i].Optional)
{
Expand All @@ -90,16 +164,17 @@ private void ApplySlots(Word inWord, int index, HashSet<Word> output)

if (_morpher.TraceManager.IsTracing)
_morpher.TraceManager.EndUnapplyTemplate(_template, inWord, true, FailureReason.None);
ReportShadow("completed-under-fired", fired, -1);
output.Add(inWord);
}

private void ParallelApplySlots(Word inWord, HashSet<Word> output)
private void ParallelApplySlots(Word inWord, HashSet<Word> output, int[] fired)
{
ParallelOptions parallelOptions = _morpher.CreateParallelOptions();
var outStack = new ConcurrentStack<Word>();
var from = new ConcurrentStack<Tuple<Word, int>>();
from.Push(Tuple.Create(inWord, _rules.Count - 1));
var to = new ConcurrentStack<Tuple<Word, int>>();
var from = new ConcurrentStack<Tuple<Word, int, int[]>>();
from.Push(Tuple.Create(inWord, _rules.Count - 1, fired));
var to = new ConcurrentStack<Tuple<Word, int, int[]>>();
while (!from.IsEmpty)
{
to.Clear();
Expand All @@ -111,9 +186,25 @@ private void ParallelApplySlots(Word inWord, HashSet<Word> output)
bool add = true;
for (int i = work.Item2; i >= 0; i--)
{
Tuple<Word, int>[] workItems = _rules[i]
int next = i - 1;
Tuple<Word, int, int[]>[] workItems = _rules[i]
.Apply(work.Item1)
.Select(res => Tuple.Create(res, i - 1))
.Select(res =>
{
ReportShadow("violated", work.Item3, i);
int[] resFired = work.Item3;
if (!RequiredSlotCannotUnapply(res, next, ref resFired))
return Tuple.Create(res, next, resFired);
if (_morpher.TraceManager.IsTracing)
_morpher.TraceManager.EndUnapplyTemplate(
_template,
res,
false,
FailureReason.None
);
return null;
})
.Where(item => item != null)
.ToArray();
if (workItems.Length > 0)
to.PushRange(workItems);
Expand Down Expand Up @@ -141,11 +232,12 @@ private void ParallelApplySlots(Word inWord, HashSet<Word> output)
true,
FailureReason.None
);
ReportShadow("completed-under-fired", work.Item3, -1);
outStack.Push(work.Item1);
}
}
);
ConcurrentStack<Tuple<Word, int>> temp = from;
ConcurrentStack<Tuple<Word, int, int[]>> temp = from;
from = to;
to = temp;
}
Expand Down
20 changes: 20 additions & 0 deletions src/SIL.Machine.Morphology.HermitCrab/Morpher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,26 @@ public ITraceManager TraceManager

public int DeletionReapplications { get; set; }

/// <summary>
/// Whether affix template walks drop partials in which a required slot can no longer unapply because
/// the material it inserts is absent. See <see cref="RequiredSlotMaterial"/>.
/// </summary>
internal RequiredSlotLookaheadMode RequiredSlotLookahead { get; set; } = RequiredSlotLookaheadMode.On;

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.

Minor: F3. Consumers of the package have no way to turn the prune off: it changes analysis for every one of them, defaults to On, and the switch is internal. If the purity argument misses a case in some grammar, parses are lost silently, with no public way to switch it off or confirm the cause, unlike DeletionReapplications beside it. Make it public, or say why not.


/// <summary>
/// Called in <see cref="RequiredSlotLookaheadMode.Shadow"/> with a kind and the template and slot it is about:
/// "fired" (the test is true for a required slot), "violated" (a rule of a fired slot then unapplied under
/// that partial) and "completed-under-fired" (the template walk completed with a fired slot on its path).
/// Any "violated" or "completed-under-fired" is a soundness failure.
/// </summary>
internal Action<string, AffixTemplate, int> RequiredSlotShadow { get; set; }

/// <summary>
/// Called after each required-slot check that unified, with the number of unifications it performed.
/// Null unless a diagnostic sets it; it may be called from several threads.
/// </summary>
internal Action<long> RequiredSlotCheckObserver { get; set; }

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: F4. RequiredSlotCheckObserver is never set in the product or tests, and Shadow mode with its string-kinded callback ("fired", "violated", "completed-under-fired") is measurement scaffolding. It threads an int[] through every walk tuple. Consider moving it to the research branch, or keeping it test-only.


public int MaxStemCount { get; set; }

/// <summary>
Expand Down
Loading
Loading