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
6 changes: 3 additions & 3 deletions src/SIL.Machine.Morphology.HermitCrab/AnalysisLanguageRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ 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;
int maxAlternatives = _morpher.MaxAlternatives;

foreach (Word inData in inputSet)
{
foreach (Word outData in _rules[i].Apply(inData, ref alternativeCount))
foreach (Word outData in _rules[i].Apply(inData, ref alternativeCount, maxAlternatives))
{
outputSet.Add(outData);
results.Add(outData);
Expand Down
93 changes: 55 additions & 38 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,28 @@ 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, 0);
}

internal IEnumerable<Word> Apply(Word input, ref int alternativeCount)
internal IEnumerable<Word> Apply(Word input, ref int alternativeCount, int maxAlternatives)
{
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);

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. With MaxAlternatives > 0, every Apply call copies both rule lists into a new cascade and a new RuleBatch. AnalysisLanguageRule makes that call once per input word per stratum, so the allocations grow with ambiguity. Building the capped pair once per stratum per parse, before the inData loop, gives the same limits. Unverified cost: not measured.

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 +148,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 +158,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 +187,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 +204,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 +232,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
59 changes: 59 additions & 0 deletions src/SIL.Machine.Morphology.HermitCrab/Morpher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,65 @@ 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.
/// 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
)
{
if (words == null)
throw new ArgumentNullException(nameof(words));
if (maxWordConcurrency < 1)

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: F2. A maxWordConcurrency below 1 throws ArgumentOutOfRangeException. Everywhere else in this class a parallelism below 1 means unbounded (MaxDegreeOfParallelism doc, CreateParallelOptions), and ParseWords_ValidatesBatchArguments asserts the throw for 0 and -1. A caller following that convention gets an exception.

throw new ArgumentOutOfRangeException(nameof(maxWordConcurrency));
if (MaxDegreeOfParallelism != 1)

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. ParseWords throws for any Morpher built with the default maxDegreeOfParallelism (0). So new Morpher(tm, lang).ParseWords(words) fails, even though the MaxDegreeOfParallelism doc in this file (L117-124) says that value "must remain a performance knob only". Running the batch serially, or with word concurrency 1, when degree != 1 would keep that contract. If the throw is deliberate, the doc should say so.

{
throw new InvalidOperationException(
"ParseWords requires a Morpher constructed with maxDegreeOfParallelism: 1."
);
}

string[] inputs = words.ToArray();
var results = new WordParseResult[inputs.Length];
void Parse(int index)
{
object trace = 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);
}
}

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
);
}

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