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 @@ -32,9 +32,8 @@ public IEnumerable<Word> Apply(Word input)
HashSet<Word> outputSet = tempSet;
outputSet.Clear();

// Limit alternatives accross all invocations of _rules[i].Apply.
// The limit spans every input to this stratum in this parse.
int alternativeCount = 0;
_rules[i].MaxAlternatives = _morpher.MaxAlternatives;

foreach (Word inData in inputSet)
{
Expand Down
97 changes: 60 additions & 37 deletions src/SIL.Machine.Morphology.HermitCrab/AnalysisStratumRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ internal class AnalysisStratumRule : IRule<Word, ShapeNode>
private readonly RuleBatch<Word, ShapeNode> _templatesRule;
private readonly Stratum _stratum;
private readonly Morpher _morpher;
private int _maxAlternatives;

public AnalysisStratumRule(Morpher morpher, Stratum stratum)
{
Expand All @@ -30,45 +29,47 @@ public AnalysisStratumRule(Morpher morpher, Stratum stratum)
false,
FreezableEqualityComparer<Word>.Default
);
_mrulesRule = null;
IEnumerable<IRule<Word, ShapeNode>> mrules = stratum
.MorphologicalRules.Select(mrule => CompileMorphologicalRule(mrule, morpher))
.Reverse();
switch (stratum.MorphologicalRuleOrder)
_mrulesRule = CreateMorphologicalRules(
stratum.MorphologicalRules.Select(mrule => CompileMorphologicalRule(mrule, morpher)).Reverse()
);
}

private RuleCascade<Word, ShapeNode> CreateMorphologicalRules(IEnumerable<IRule<Word, ShapeNode>> rules)
{
switch (_stratum.MorphologicalRuleOrder)
{
case MorphologicalRuleOrder.Linear:
// Use PermutationRuleCascade instead of LinearRuleCascade
// because morphological rules should be considered optional
// during unapplication (they are obligatory during application,
// but we don't know they have been applied during unapplication).
_mrulesRule = new PermutationRuleCascade<Word, ShapeNode>(
mrules,
return new PermutationRuleCascade<Word, ShapeNode>(
rules,
true,
FreezableEqualityComparer<Word>.Default
);
break;
case MorphologicalRuleOrder.Unordered:
if (morpher.MaxDegreeOfParallelism == 1)
if (_morpher.MaxDegreeOfParallelism == 1)
{
_mrulesRule = new CombinationRuleCascade<Word, ShapeNode>(
mrules,
return new CombinationRuleCascade<Word, ShapeNode>(
rules,
true,
FreezableEqualityComparer<Word>.Default
);
}
else
{
_mrulesRule = new ParallelCombinationRuleCascade<Word, ShapeNode>(
mrules,
return new ParallelCombinationRuleCascade<Word, ShapeNode>(
rules,
true,
FreezableEqualityComparer<Word>.Default
)
{
MaxDegreeOfParallelism = morpher.MaxDegreeOfParallelism,
MaxDegreeOfParallelism = _morpher.MaxDegreeOfParallelism,
};
}
break;
}
return null;
}

private IRule<Word, ShapeNode> CompileAffixTemplate(AffixTemplate template, Morpher morpher)
Expand Down Expand Up @@ -107,25 +108,34 @@ private IRule<Word, ShapeNode> CompilePhonologicalRule(IPhonologicalRule prule,
}
}

public int MaxAlternatives
{
get { return _maxAlternatives; }
set
{
_maxAlternatives = value;
_mrulesRule.MaxAlternatives = value;
_templatesRule.MaxAlternatives = value;
}
}

public IEnumerable<Word> Apply(Word input)
{
// Standalone stratum application is uncapped.
int alternativeCount = 0;
return Apply(input, ref alternativeCount);
return Apply(input, ref alternativeCount, false);
}

internal IEnumerable<Word> Apply(Word input, ref int alternativeCount)
{
return Apply(input, ref alternativeCount, true);
}

private IEnumerable<Word> Apply(Word input, ref int alternativeCount, bool enforceLimit)
{
int maxAlternatives = enforceLimit ? _morpher.MaxAlternatives : 0;
RuleCascade<Word, ShapeNode> morphologicalRules = _mrulesRule;
RuleBatch<Word, ShapeNode> templates = _templatesRule;
if (maxAlternatives > 0)
{
// Limits belong to this invocation; compiled child rules remain shared and read-only.
morphologicalRules = CreateMorphologicalRules(_mrulesRule.Rules);
Comment thread
claude[bot] marked this conversation as resolved.
Resolved
morphologicalRules.MaxAlternatives = maxAlternatives;
templates = new RuleBatch<Word, ShapeNode>(_templatesRule.Rules, false, _templatesRule.Comparer)
{
MaxAlternatives = maxAlternatives,
};
}

if (_morpher.TraceManager.IsTracing)
_morpher.TraceManager.BeginUnapplyStratum(_stratum, input);

Expand All @@ -144,7 +154,8 @@ internal IEnumerable<Word> Apply(Word input, ref int alternativeCount)
wordCache = new Dictionary<AnalysisMergeKey, Word>();

// AnalysisStratumRule.Apply should cover the inverse of SynthesisStratumRule.Apply.
IEnumerable<Word> mruleOutWords = ApplyTemplates(input).Concat(ApplyMorphologicalRules(input));
IEnumerable<Word> mruleOutWords = ApplyTemplates(input, morphologicalRules, templates)
.Concat(ApplyMorphologicalRules(input, morphologicalRules, templates));
Debug.Assert(mruleOutWords != null);

var output = new HashSet<Word>(FreezableEqualityComparer<Word>.Default) { input };
Expand All @@ -153,7 +164,7 @@ internal IEnumerable<Word> Apply(Word input, ref int alternativeCount)
foreach (Word mruleOutWord in mruleOutWords)
{
alternativeCount++;
if (_maxAlternatives > 0 && alternativeCount > _maxAlternatives)
if (maxAlternatives > 0 && alternativeCount > maxAlternatives)
{
// Stops before full enumeration because ApplyTemplates and ApplyMorphologicalRules use yield return.
throw new MaxAlternativesExceededException("MaxAlternatives exceeded");
Expand Down Expand Up @@ -182,9 +193,15 @@ internal IEnumerable<Word> Apply(Word input, ref int alternativeCount)
return output;
}

private IEnumerable<Word> ApplyMorphologicalRules(Word input)
private IEnumerable<Word> ApplyMorphologicalRules(
Word input,
RuleCascade<Word, ShapeNode> morphologicalRules,
RuleBatch<Word, ShapeNode> templates
)
{
foreach (Word mruleOutWord in _mrulesRule.Apply(input).Distinct(FreezableEqualityComparer<Word>.Default))
foreach (
Word mruleOutWord in morphologicalRules.Apply(input).Distinct(FreezableEqualityComparer<Word>.Default)
)
{
switch (_stratum.MorphologicalRuleOrder)
{
Expand All @@ -193,22 +210,26 @@ private IEnumerable<Word> ApplyMorphologicalRules(Word input)
break;

case MorphologicalRuleOrder.Unordered:
foreach (Word tempOutWord in ApplyTemplates(mruleOutWord))
foreach (Word tempOutWord in ApplyTemplates(mruleOutWord, morphologicalRules, templates))
yield return tempOutWord;
yield return mruleOutWord;
break;
}
}
}

private IEnumerable<Word> ApplyTemplates(Word input)
private IEnumerable<Word> ApplyTemplates(
Word input,
RuleCascade<Word, ShapeNode> morphologicalRules,
RuleBatch<Word, ShapeNode> templates
)
{
foreach (Word tempOutWord in _templatesRule.Apply(input).Distinct(FreezableEqualityComparer<Word>.Default))
foreach (Word tempOutWord in templates.Apply(input).Distinct(FreezableEqualityComparer<Word>.Default))
{
switch (_stratum.MorphologicalRuleOrder)
{
case MorphologicalRuleOrder.Linear:
foreach (Word outWord in ApplyMorphologicalRules(tempOutWord))
foreach (Word outWord in ApplyMorphologicalRules(tempOutWord, morphologicalRules, templates))
yield return outWord;
if (!FreezableEqualityComparer<Word>.Default.Equals(input, tempOutWord))
yield return tempOutWord;
Expand All @@ -217,7 +238,9 @@ private IEnumerable<Word> ApplyTemplates(Word input)
case MorphologicalRuleOrder.Unordered:
if (!FreezableEqualityComparer<Word>.Default.Equals(input, tempOutWord))
{
foreach (Word outWord in ApplyMorphologicalRules(tempOutWord))
foreach (
Word outWord in ApplyMorphologicalRules(tempOutWord, morphologicalRules, templates)
)
yield return outWord;
yield return tempOutWord;
}
Expand Down
91 changes: 89 additions & 2 deletions src/SIL.Machine.Morphology.HermitCrab/Morpher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ public class Morpher : IMorphologicalAnalyzer, IMorphologicalGenerator
private readonly IList<RootAllomorph> _lexicalPatterns = new List<RootAllomorph>();

public Morpher(ITraceManager traceManager, Language lang, int maxDegreeOfParallelism = 0)
: this(traceManager, lang, maxDegreeOfParallelism, 0) { }

public Morpher(ITraceManager traceManager, Language lang, int maxDegreeOfParallelism, int maxAlternatives)
{
MaxAlternatives = maxAlternatives;
_lang = lang;
_traceManager = traceManager;
// Must be set before CompileAnalysisRule: AnalysisStratumRule picks a sequential vs. parallel
Expand Down Expand Up @@ -60,7 +64,6 @@ public Morpher(ITraceManager traceManager, Language lang, int maxDegreeOfParalle
_analysisRule = lang.CompileAnalysisRule(this);
_synthesisRule = lang.CompileSynthesisRule(this);
MaxStemCount = 2;
MaxAlternatives = 0;
MergeEquivalentAnalyses = true;
PruneDisagreeingCopies = true;
LexEntrySelector = entry => true;
Expand Down Expand Up @@ -93,8 +96,9 @@ public ITraceManager TraceManager
/// <summary>
/// MaxAlternatives limits the number of alternatives considered.
/// If the limit is exceeded, then MaxAlternativesExceededException is thrown.
/// Set this value with the constructor.
/// </summary>
public int MaxAlternatives { get; set; }
public int MaxAlternatives { get; }

/// <summary>
/// Merge analyses with equivalent state for every analysis-side rule.
Expand Down Expand Up @@ -128,6 +132,7 @@ public ITraceManager TraceManager
/// (a value of 1 compiles the sequential cascade for unordered strata). It must remain a performance
/// knob only: nothing that changes which analyses a parse returns, or whether
/// <see cref="MaxAlternatives"/> is exceeded, may depend on it.
/// <see cref="ParseWords"/> throws unless this value is 1.
/// </summary>
public int MaxDegreeOfParallelism { get; }

Expand Down Expand Up @@ -225,6 +230,88 @@ public IEnumerable<Word> ParseWord(string word, out object trace, bool guessRoot
return syntheses;
}

/// <summary>
/// Eagerly parses surface forms in input order, capturing each parse's exception and trace in its result.
/// Requires a Morpher constructed with maxDegreeOfParallelism: 1. Tracing runs the batch serially.
/// Keep the Language, settings, and tracing fixed until completion; selectors and custom rules must
/// support concurrent calls when maxWordConcurrency is greater than 1 and tracing is disabled.
/// Do not overlap traced calls sharing a trace manager. Failed parses have empty analyses.
/// Progress receives the input index and surface form, with a null result at the start and the
/// completed result at the finish. It runs on parsing threads (the calling thread for serial batches),
/// possibly concurrently, and must be fast and thread-safe. Callback exceptions are collected and
/// propagated as an AggregateException after all words and both notifications have been processed.
/// Batch argument validation and input enumeration errors propagate to the caller.
/// </summary>
public IReadOnlyList<WordParseResult> ParseWords(
IEnumerable<string> words,
int maxWordConcurrency = 4,
bool guessRoot = false,
Action<int, string, WordParseResult> progress = null
)
{
if (words == null)
throw new ArgumentNullException(nameof(words));
if (maxWordConcurrency < 1)
Comment thread
claude[bot] marked this conversation as resolved.
Resolved
throw new ArgumentOutOfRangeException(nameof(maxWordConcurrency));
if (MaxDegreeOfParallelism != 1)
Comment thread
claude[bot] marked this conversation as resolved.
Resolved
{
throw new InvalidOperationException(
"ParseWords requires a Morpher constructed with maxDegreeOfParallelism: 1."
);
}

string[] inputs = words.ToArray();
var results = new WordParseResult[inputs.Length];
var callbackErrors = new ConcurrentQueue<Exception>();
void Notify(int index, WordParseResult result)
{
try
{
progress?.Invoke(index, inputs[index], result);
}
catch (Exception error)
{
callbackErrors.Enqueue(error);
}
}

void Parse(int index)
{
object trace = null;
Notify(index, null);
try
{
var analyses = ParseWord(inputs[index], out trace, guessRoot).ToList().AsReadOnly();
results[index] = new WordParseResult(inputs[index], analyses, trace, null);
}
catch (Exception error)
{
results[index] = new WordParseResult(inputs[index], Array.Empty<Word>(), trace, error);
}
Notify(index, results[index]);
}

if (_traceManager.IsTracing || maxWordConcurrency == 1)
{
for (int i = 0; i < inputs.Length; i++)
Parse(i);
}
else
{
Parallel.For(
0,
inputs.Length,
new ParallelOptions { MaxDegreeOfParallelism = maxWordConcurrency },
Parse
);
}

if (!callbackErrors.IsEmpty)
throw new AggregateException("One or more progress callbacks failed.", callbackErrors);

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. One failing progress callback throws away the whole batch: every word is parsed, but the AggregateException carries no results, so the caller has to parse them all again. A per-word parse failure, by contrast, is kept in its WordParseResult. ParseWords_PropagatesCallbackErrorsAfterAllWordsFinish only gets its results back by copying them inside the callback. Returning the results anyway, or attaching them to the exception, would keep them.


return Array.AsReadOnly(results);
}

/// <summary>
/// Generates surface forms from the specified word synthesis information.
/// </summary>
Expand Down
24 changes: 24 additions & 0 deletions src/SIL.Machine.Morphology.HermitCrab/WordParseResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;

namespace SIL.Machine.Morphology.HermitCrab
{
public sealed class WordParseResult
{
internal WordParseResult(string surfaceForm, IReadOnlyList<Word> analyses, object trace, Exception error)
{
SurfaceForm = surfaceForm;
Analyses = analyses;
Trace = trace;
Error = error;
}

public string SurfaceForm { get; }

public IReadOnlyList<Word> Analyses { get; }

public object Trace { get; }

public Exception Error { get; }
}
}
Loading
Loading