diff --git a/conformance/fieldworks-producibility.tsv b/conformance/fieldworks-producibility.tsv
index 7aa9f03ac..f7ac2e22f 100644
--- a/conformance/fieldworks-producibility.tsv
+++ b/conformance/fieldworks-producibility.tsv
@@ -34,6 +34,10 @@ failure-reason BoundRoot Yes HCLoader.cs:835-841 (LoadRootAllomorph, IsBound for
failure-reason NonPartialRuleProhibitedAfterFinalTemplate Yes HCLoader.cs:1678 (AffixTemplate.IsFinal from LibLCM template.Final); HCLoader.cs:708,982,1013 (rule IsPartial sites) Corroborated by HCLoaderTests.cs:750,789 (IsFinal). Reachable whenever a non-final template is followed by a non-partial, non-template rule such as a derivational affix or clitic.
failure-reason NonPartialRuleRequiredAfterNonFinalTemplate Yes HCLoader.cs:1678 (AffixTemplate.IsFinal); HCLoader.cs:708,982,1013 (rule IsPartial sites) Same drivers as NonPartialRuleProhibitedAfterFinalTemplate, opposite polarity of the same engine check.
failure-reason MaxApplicationCount Yes HCLoader.cs:103-112 (ParserParameters CompoundRules/maxApps parsed into m_CompoundRuleLookup); HCLoader.cs:1894-1896 (CompoundingRule.MaxApplicationCount assignment) Only ever wired for CompoundingRule, not AffixProcessRule (grep-confirmed single assignment site) -- still reachable via the compounding path.
+failure-reason OutputSyntacticFeatureStruct Yes HCLoader.cs:993,1828 at FieldWorks bd0608da7 (AffixProcessRule.OutSyntacticFeatureStruct); HCLoader.cs:1939,2004,2031 at FieldWorks bd0608da7 (CompoundingRule.OutSyntacticFeatureStruct) Raised when unapplying an affix or compounding rule whose output features do not unify with the word's.
+failure-reason RealizationalFeatureStruct No Raised only by AnalysisRealizationalAffixProcessRule, and HCLoader never builds a RealizationalAffixProcessRule (no reference anywhere in Src/LexText/ParserCore at FieldWorks bd0608da7); see the RealizationalRule loader-gap row.
+failure-reason MaxStemCount Yes HCParser.cs:198 at FieldWorks bd0608da7 (Morpher.MaxStemCount from the parser parameters); HCLoader.cs:248-250,1932,1997,2024 at FieldWorks bd0608da7 (default and user compounding rules) Raised by compounding unapplication once the non-head count reaches the cap; every FieldWorks project gets compounding rules, the defaults when it defines none.
+failure-reason NonHeadLexicalLookup Yes HCLoader.cs:248-250,1932,1997,2024 at FieldWorks bd0608da7 (default and user compounding rules) Raised when a compounding rule's unapplied non-head matches no root allomorph.
interface-attribute AffixTemplate.requiredPartsOfSpeech Yes HCLoader.cs:1681-1684 (LoadAffixTemplate)
interface-attribute AffixTemplate.requiredSubcategorizedRules No Subcategorization/SyntacticRule is dead schema across the WHOLE engine, not just HCLoader: grepping src/SIL.Machine.Morphology.HermitCrab for 'Subcategoriz' outside the DTD file itself returns zero hits (no C# class implements it), and HCLoader.cs / HCLoaderTests.cs likewise have zero hits for 'Subcategoriz' or 'SyntacticRule'. Confirmed by this repo's own InteractionChainLedger.cs comments ('dead (subcategorization)') on all 9 subcategorization-family attributes (rows 12,20,23,27,35(no -- separate reason),40,52,55,56,57).
interface-attribute Allomorph.stemName Yes HCLoader.cs:831-833 (LoadRootAllomorph) Corroborated by HCLoaderTests.cs:817.
diff --git a/conformance/tools/generate-fieldworks-producibility.ps1 b/conformance/tools/generate-fieldworks-producibility.ps1
index 5048f4f69..86464d8cf 100644
--- a/conformance/tools/generate-fieldworks-producibility.ps1
+++ b/conformance/tools/generate-fieldworks-producibility.ps1
@@ -58,7 +58,7 @@ if (-not (Test-Path $TraceManagerPath)) { throw "Cannot find $TraceManagerPath"
if (-not (Test-Path $InterfaceInventoryPath)) { throw "Cannot find $InterfaceInventoryPath" }
# ---------------------------------------------------------------------------
-# 1. Mechanically extract the FailureReason subjects (23, excluding None).
+# 1. Mechanically extract the FailureReason subjects (27, excluding None).
# ---------------------------------------------------------------------------
$traceManagerText = Get-Content $TraceManagerPath -Raw
$enumMatch = [regex]::Match($traceManagerText, "public enum FailureReason\s*\{(?
[^}]*)\}")
@@ -67,8 +67,8 @@ $failureReasons = $enumMatch.Groups["body"].Value -split "," |
ForEach-Object { $_.Trim() } |
Where-Object { $_ -and $_ -ne "None" }
-if ($failureReasons.Count -ne 23) {
- throw "Expected 23 FailureReason members besides None, found $($failureReasons.Count): $($failureReasons -join ', ')"
+if ($failureReasons.Count -ne 27) {
+ throw "Expected 27 FailureReason members besides None, found $($failureReasons.Count): $($failureReasons -join ', ')"
}
# ---------------------------------------------------------------------------
@@ -247,6 +247,25 @@ Add-Verdict "failure-reason" "MaxApplicationCount" "Yes" @(
"HCLoader.cs:1894-1896 (CompoundingRule.MaxApplicationCount assignment)"
) "Only ever wired for CompoundingRule, not AffixProcessRule (grep-confirmed single assignment site) -- still reachable via the compounding path."
+Add-Verdict "failure-reason" "OutputSyntacticFeatureStruct" "Yes" @(
+ "HCLoader.cs:993,1828 at FieldWorks bd0608da7 (AffixProcessRule.OutSyntacticFeatureStruct)"
+ "HCLoader.cs:1939,2004,2031 at FieldWorks bd0608da7 (CompoundingRule.OutSyntacticFeatureStruct)"
+) "Raised when unapplying an affix or compounding rule whose output features do not unify with the word's."
+
+Add-Verdict "failure-reason" "MaxStemCount" "Yes" @(
+ "HCParser.cs:198 at FieldWorks bd0608da7 (Morpher.MaxStemCount from the parser parameters)"
+ "HCLoader.cs:248-250,1932,1997,2024 at FieldWorks bd0608da7 (default and user compounding rules)"
+) "Raised by compounding unapplication once the non-head count reaches the cap; every FieldWorks project gets compounding rules, the defaults when it defines none."
+
+Add-Verdict "failure-reason" "NonHeadLexicalLookup" "Yes" @(
+ "HCLoader.cs:248-250,1932,1997,2024 at FieldWorks bd0608da7 (default and user compounding rules)"
+) "Raised when a compounding rule's unapplied non-head matches no root allomorph."
+
+Add-Verdict "failure-reason" "RealizationalFeatureStruct" "No" @() (
+ "Raised only by AnalysisRealizationalAffixProcessRule, and HCLoader never builds a RealizationalAffixProcessRule " +
+ "(no reference anywhere in Src/LexText/ParserCore at FieldWorks bd0608da7); see the RealizationalRule loader-gap row."
+)
+
# --- Interface-inventory subjects (subject_kind = interface-attribute) ------
Add-Verdict "interface-attribute" "AffixTemplate.requiredPartsOfSpeech" "Yes" @(
@@ -726,7 +745,7 @@ if ($staleKeys) { throw "Verdict table has entries with no matching subject (sta
$dupes = $rows | Group-Object subject_kind, subject | Where-Object { $_.Count -gt 1 }
if ($dupes) { throw "Duplicate subjects in output: $($dupes.Name -join '; ')" }
-if ($rows.Count -ne 88) { throw "Expected 88 total rows (23 failure-reason + 60 interface-attribute + 5 loader-gap), got $($rows.Count)" }
+if ($rows.Count -ne 92) { throw "Expected 92 total rows (27 failure-reason + 60 interface-attribute + 5 loader-gap), got $($rows.Count)" }
# ---------------------------------------------------------------------------
# 5. Emit the TSV.
diff --git a/src/SIL.Machine.Morphology.HermitCrab.Conformance/FailureRuleAttributor.cs b/src/SIL.Machine.Morphology.HermitCrab.Conformance/FailureRuleAttributor.cs
index 8e3b69509..bd6a8c2ec 100644
--- a/src/SIL.Machine.Morphology.HermitCrab.Conformance/FailureRuleAttributor.cs
+++ b/src/SIL.Machine.Morphology.HermitCrab.Conformance/FailureRuleAttributor.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using SIL.Machine.Morphology.HermitCrab.MorphologicalRules;
namespace SIL.Machine.Morphology.HermitCrab.Conformance;
@@ -11,19 +12,21 @@ namespace SIL.Machine.Morphology.HermitCrab.Conformance;
/// asserted to VERIFIED, the same standard a crash-attributed rule is held to elsewhere in the
/// coverage report.
///
-/// One trace shape is used, inherently self-limiting rather than filtered by
-/// value: a node
-/// (Word.CheckBlocking substituted a suppletive family partner) can only ever appear AFTER
-/// the rule already applied successfully to build that specific candidate -- it cannot fire for a
-/// rule that was never in scope for this word, so it carries rule identity directly on
-/// with no risk of blanket noise.
-///
-/// Not yet used: a node's allomorph. An
-/// Environments/AllomorphCoOccurrenceRules/MorphemeCoOccurrenceRules failure
-/// is checked in Morpher.IsWordValid only against allomorphs already part of a
-/// fully-synthesized candidate, so the affix rule owning that allomorph genuinely applied and would
-/// be a safe attribution. The trace does not record which allomorph failed until
-/// sillsdev/machine#534 lands; such words stay label-only until then.
+/// Two trace shapes are used, both inherently self-limiting rather than filtered by
+/// value:
+///
+/// - A node (Word.CheckBlocking substituted a suppletive
+/// family partner) can only ever appear AFTER the rule already applied successfully to build that
+/// specific candidate -- it cannot fire for a rule that was never in scope for this word, so it
+/// carries rule identity directly on with no risk of blanket noise.
+/// - A node whose resolves to
+/// an is likewise self-limiting: Environments/
+/// AllomorphCoOccurrenceRules/MorphemeCoOccurrenceRules are checked in
+/// Morpher.IsWordValid only against allomorphs that are ALREADY PART OF a fully-synthesized
+/// final candidate, so the rule that owns the allomorph ( -- an
+/// / is itself a
+/// ) genuinely did apply to build that candidate, not merely get tried.
+///
///
/// Deliberately NOT used: a /
/// /
@@ -84,6 +87,15 @@ void Walk(Trace node)
if (node.FailureReason == FailureReason.None && surfaceFormMismatch)
AddPhonological(node.Source);
break;
+ case TraceType.Failed:
+ if (
+ node.Allomorph is AffixProcessAllomorph affixAllo
+ && affixAllo.Morpheme is IMorphologicalRule mrule
+ )
+ {
+ AddMorphological(mrule);
+ }
+ break;
}
foreach (Trace child in node.Children)
Walk(child);
diff --git a/src/SIL.Machine.Morphology.HermitCrab.Tool/ParseCommand.cs b/src/SIL.Machine.Morphology.HermitCrab.Tool/ParseCommand.cs
index 86bfc0db1..df6ea1e81 100644
--- a/src/SIL.Machine.Morphology.HermitCrab.Tool/ParseCommand.cs
+++ b/src/SIL.Machine.Morphology.HermitCrab.Tool/ParseCommand.cs
@@ -140,7 +140,9 @@ private static bool IsAnalysis(TraceType type)
case TraceType.StratumAnalysisOutput:
case TraceType.TemplateAnalysisInput:
case TraceType.TemplateAnalysisOutput:
+ case TraceType.TemplateSlotAnalysis:
case TraceType.MorphologicalRuleAnalysis:
+ case TraceType.CompoundingRuleAnalysis:
case TraceType.PhonologicalRuleAnalysis:
return true;
@@ -152,7 +154,9 @@ private static bool IsAnalysis(TraceType type)
case TraceType.StratumSynthesisOutput:
case TraceType.TemplateSynthesisInput:
case TraceType.TemplateSynthesisOutput:
+ case TraceType.TemplateSlotSynthesis:
case TraceType.MorphologicalRuleSynthesis:
+ case TraceType.CompoundingRuleSynthesis:
case TraceType.PhonologicalRuleSynthesis:
return false;
}
@@ -203,6 +207,14 @@ private static string GetTraceTypeString(TraceType type)
return "Template Synthesis In";
case TraceType.TemplateSynthesisOutput:
return "Template Synthesis Out";
+ case TraceType.TemplateSlotAnalysis:
+ return "Template Slot Analysis";
+ case TraceType.TemplateSlotSynthesis:
+ return "Template Slot Synthesis";
+ case TraceType.CompoundingRuleAnalysis:
+ return "Compounding Rule Analysis";
+ case TraceType.CompoundingRuleSynthesis:
+ return "Compounding Rule Synthesis";
case TraceType.MorphologicalRuleAnalysis:
return "Morphological Rule Analysis";
case TraceType.MorphologicalRuleSynthesis:
diff --git a/src/SIL.Machine.Morphology.HermitCrab/AnalysisAffixTemplateRule.cs b/src/SIL.Machine.Morphology.HermitCrab/AnalysisAffixTemplateRule.cs
index 9044a8488..5d666ecd1 100644
--- a/src/SIL.Machine.Morphology.HermitCrab/AnalysisAffixTemplateRule.cs
+++ b/src/SIL.Machine.Morphology.HermitCrab/AnalysisAffixTemplateRule.cs
@@ -77,8 +77,18 @@ private void ApplySlots(Word inWord, int index, HashSet output)
{
for (int i = index; i >= 0; i--)
{
+ bool producedOutput = false;
foreach (Word outWord in _rules[i].Apply(inWord))
+ {
+ producedOutput = true;
+ TraceSlot(inWord, outWord, i, TemplateSlotOutcome.RuleApplied);
ApplySlots(outWord, i - 1, output);
+ }
+
+ if (_template.Slots[i].Optional)
+ TraceSlot(inWord, null, i, TemplateSlotOutcome.OptionalSkipped);
+ else if (!producedOutput)
+ TraceSlot(inWord, null, i, TemplateSlotOutcome.RequiredUnfilled);
if (!_template.Slots[i].Optional)
{
@@ -116,7 +126,16 @@ private void ParallelApplySlots(Word inWord, HashSet output)
.Select(res => Tuple.Create(res, i - 1))
.ToArray();
if (workItems.Length > 0)
+ {
+ foreach (Tuple item in workItems)
+ TraceSlot(work.Item1, item.Item1, i, TemplateSlotOutcome.RuleApplied);
to.PushRange(workItems);
+ }
+
+ if (_template.Slots[i].Optional)
+ TraceSlot(work.Item1, null, i, TemplateSlotOutcome.OptionalSkipped);
+ else if (workItems.Length == 0)
+ TraceSlot(work.Item1, null, i, TemplateSlotOutcome.RequiredUnfilled);
if (!_template.Slots[i].Optional)
{
@@ -152,5 +171,11 @@ private void ParallelApplySlots(Word inWord, HashSet output)
output.UnionWith(outStack);
}
+
+ private void TraceSlot(Word input, Word output, int index, TemplateSlotOutcome outcome)
+ {
+ if (_morpher.TraceManager.IsTracing)
+ _morpher.TraceManager.TemplateSlotProcessed(_template, index, input, output, true, outcome);
+ }
}
}
diff --git a/src/SIL.Machine.Morphology.HermitCrab/IDetailedTraceManager.cs b/src/SIL.Machine.Morphology.HermitCrab/IDetailedTraceManager.cs
new file mode 100644
index 000000000..fe741565e
--- /dev/null
+++ b/src/SIL.Machine.Morphology.HermitCrab/IDetailedTraceManager.cs
@@ -0,0 +1,63 @@
+namespace SIL.Machine.Morphology.HermitCrab
+{
+ public enum TemplateSlotOutcome
+ {
+ RuleApplied,
+ OptionalSkipped,
+ RequiredUnfilled,
+ }
+
+ public enum PartialParseCause
+ {
+ RealizationalFeatureMismatch,
+ UnappliedMorphologicalRules,
+ NonFinalTemplateAppliedLast,
+ ApplicableTemplatesNotApplied,
+ }
+
+ public class PartialParseFailure
+ {
+ public PartialParseFailure(PartialParseCause cause, IMorphologicalRule rule = null)
+ {
+ Cause = cause;
+ Rule = rule;
+ }
+
+ public PartialParseCause Cause { get; }
+
+ public IMorphologicalRule Rule { get; }
+ }
+
+ ///
+ /// Optional tracing capability. ITraceManager implementations receive the existing callbacks without opting in.
+ ///
+ public interface IDetailedTraceManager : ITraceManager
+ {
+ void MorphologicalRuleNotUnapplied(
+ IMorphologicalRule rule,
+ int subruleIndex,
+ Word input,
+ FailureReason reason,
+ object failureObj
+ );
+
+ ///
+ /// Reports fully enumerated lookup results as yielded Word candidates, before surface validation.
+ /// Interrupted lookups do not receive this callback. Each input has at most one active lookup.
+ ///
+ void LexicalLookupCompleted(Stratum stratum, Word input, int candidateCount, bool guessed);
+
+ ///
+ /// Reports a slot branch. OptionalSkipped can accompany RuleApplied for the same input.
+ /// RequiredUnfilled means the slot produced no output; no linguistic cause is implied.
+ ///
+ void TemplateSlotProcessed(
+ AffixTemplate template,
+ int slotIndex,
+ Word input,
+ Word output,
+ bool analysis,
+ TemplateSlotOutcome outcome
+ );
+ }
+}
diff --git a/src/SIL.Machine.Morphology.HermitCrab/ITraceManager.cs b/src/SIL.Machine.Morphology.HermitCrab/ITraceManager.cs
index f94dbdcb1..fe3d73f24 100644
--- a/src/SIL.Machine.Morphology.HermitCrab/ITraceManager.cs
+++ b/src/SIL.Machine.Morphology.HermitCrab/ITraceManager.cs
@@ -26,6 +26,10 @@ public enum FailureReason
NonPartialRuleProhibitedAfterFinalTemplate,
NonPartialRuleRequiredAfterNonFinalTemplate,
MaxApplicationCount,
+ OutputSyntacticFeatureStruct,
+ RealizationalFeatureStruct,
+ MaxStemCount,
+ NonHeadLexicalLookup,
}
public interface ITraceManager
diff --git a/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs b/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs
index 1cddacd0a..5a2084b6a 100644
--- a/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs
+++ b/src/SIL.Machine.Morphology.HermitCrab/Morpher.cs
@@ -444,6 +444,7 @@ internal IEnumerable SearchRootAllomorphs(Stratum stratum, Shape
private IEnumerable LexicalLookup(Word input)
{
+ int candidateCount = 0;
if (_traceManager.IsTracing)
_traceManager.LexicalLookup(input.Stratum, input);
foreach (
@@ -464,9 +465,12 @@ LexEntry entry in SearchRootAllomorphs(input.Stratum, input.Shape)
if (_traceManager.IsTracing)
_traceManager.SynthesizeWord(_lang, newWord);
newWord.Freeze();
+ candidateCount++;
yield return newWord;
}
}
+ if (_traceManager.IsTracing)
+ _traceManager.LexicalLookupCompleted(input.Stratum, input, candidateCount, false);
}
///
@@ -474,6 +478,7 @@ LexEntry entry in SearchRootAllomorphs(input.Stratum, input.Shape)
///
private IEnumerable LexicalGuess(Word input)
{
+ int candidateCount = 0;
if (_traceManager.IsTracing)
_traceManager.LexicalLookup(input.Stratum, input);
CharacterDefinitionTable table = input.Stratum.CharacterDefinitionTable;
@@ -540,10 +545,13 @@ private IEnumerable LexicalGuess(Word input)
if (_traceManager.IsTracing)
_traceManager.SynthesizeWord(_lang, newWord);
newWord.Freeze();
+ candidateCount++;
yield return newWord;
}
}
}
+ if (_traceManager.IsTracing)
+ _traceManager.LexicalLookupCompleted(input.Stratum, input, candidateCount, true);
}
///
@@ -657,13 +665,31 @@ private IEnumerable GetShapeStrings(IList nodes, CharacterDef
private bool IsWordValid(Word word)
{
- if (
- !word.RealizationalFeatureStruct.IsUnifiable(word.SyntacticFeatureStruct)
- || !word.IsAllMorphologicalRulesApplied
- )
+ if (!word.RealizationalFeatureStruct.IsUnifiable(word.SyntacticFeatureStruct))
+ {
+ if (_traceManager.IsTracing)
+ _traceManager.Failed(
+ _lang,
+ word,
+ FailureReason.PartialParse,
+ null,
+ new PartialParseFailure(PartialParseCause.RealizationalFeatureMismatch)
+ );
+ return false;
+ }
+ if (!word.IsAllMorphologicalRulesApplied)
{
if (_traceManager.IsTracing)
- _traceManager.Failed(_lang, word, FailureReason.PartialParse, null, null);
+ _traceManager.Failed(
+ _lang,
+ word,
+ FailureReason.PartialParse,
+ null,
+ new PartialParseFailure(
+ PartialParseCause.UnappliedMorphologicalRules,
+ word.NextMorphologicalRule
+ )
+ );
return false;
}
diff --git a/src/SIL.Machine.Morphology.HermitCrab/MorphologicalRules/AnalysisAffixProcessRule.cs b/src/SIL.Machine.Morphology.HermitCrab/MorphologicalRules/AnalysisAffixProcessRule.cs
index 11980053c..37b104030 100644
--- a/src/SIL.Machine.Morphology.HermitCrab/MorphologicalRules/AnalysisAffixProcessRule.cs
+++ b/src/SIL.Machine.Morphology.HermitCrab/MorphologicalRules/AnalysisAffixProcessRule.cs
@@ -49,20 +49,38 @@ public IEnumerable Apply(Word input)
if (!_morpher.RuleSelector(_rule))
return Enumerable.Empty();
- if (
- input.GetUnapplicationCount(_rule) >= _rule.MaxApplicationCount
- || !_outputFS.IsUnifiable(input.SyntacticFeatureStruct)
- )
+ if (input.GetUnapplicationCount(_rule) >= _rule.MaxApplicationCount)
{
+ if (_morpher.TraceManager.IsTracing)
+ _morpher.TraceManager.MorphologicalRuleNotUnapplied(
+ _rule,
+ -1,
+ input,
+ FailureReason.MaxApplicationCount,
+ _rule.MaxApplicationCount
+ );
+ return Enumerable.Empty();
+ }
+ if (!_outputFS.IsUnifiable(input.SyntacticFeatureStruct))
+ {
+ if (_morpher.TraceManager.IsTracing)
+ _morpher.TraceManager.MorphologicalRuleNotUnapplied(
+ _rule,
+ -1,
+ input,
+ FailureReason.OutputSyntacticFeatureStruct,
+ _outputFS
+ );
return Enumerable.Empty();
}
var output = new List();
for (int i = 0; i < _rules.Count; i++)
{
- bool unapplied = false;
+ bool matched = false;
foreach (Word outWord in _rules[i].Apply(input).RemoveDuplicates())
{
+ matched = true;
// the paths Out writes describe this rule's output, not the stem it applied to, so they are
// stripped rather than carried backwards; what is left must unify with Required
outWord.SyntacticFeatureStruct.Restrict(_rule.OutSyntacticFeatureStruct);
@@ -75,6 +93,14 @@ out FeatureStruct syntacticFS
)
)
{
+ if (_morpher.TraceManager.IsTracing)
+ _morpher.TraceManager.MorphologicalRuleNotUnapplied(
+ _rule,
+ i,
+ outWord,
+ FailureReason.RequiredSyntacticFeatureStruct,
+ _rule.RequiredSyntacticFeatureStruct
+ );
continue;
}
@@ -91,11 +117,10 @@ out FeatureStruct syntacticFS
if (_morpher.TraceManager.IsTracing)
_morpher.TraceManager.MorphologicalRuleUnapplied(_rule, i, input, outWord);
output.Add(outWord);
- unapplied = true;
}
- if (_morpher.TraceManager.IsTracing && !unapplied)
- _morpher.TraceManager.MorphologicalRuleNotUnapplied(_rule, i, input);
+ if (_morpher.TraceManager.IsTracing && !matched)
+ _morpher.TraceManager.MorphologicalRuleNotUnapplied(_rule, i, input, FailureReason.Pattern, null);
}
return output;
}
diff --git a/src/SIL.Machine.Morphology.HermitCrab/MorphologicalRules/AnalysisCompoundingRule.cs b/src/SIL.Machine.Morphology.HermitCrab/MorphologicalRules/AnalysisCompoundingRule.cs
index f77bf96e4..c9267ead7 100644
--- a/src/SIL.Machine.Morphology.HermitCrab/MorphologicalRules/AnalysisCompoundingRule.cs
+++ b/src/SIL.Machine.Morphology.HermitCrab/MorphologicalRules/AnalysisCompoundingRule.cs
@@ -48,12 +48,19 @@ public IEnumerable Apply(Word input)
if (!_morpher.RuleSelector(_rule))
return Enumerable.Empty();
- if (
- input.NonHeadCount + 1 >= _morpher.MaxStemCount
- || input.GetUnapplicationCount(_rule) >= _rule.MaxApplicationCount
- || !_outputFS.IsUnifiable(input.SyntacticFeatureStruct)
- )
+ if (input.NonHeadCount + 1 >= _morpher.MaxStemCount)
{
+ TraceFailure(-1, input, FailureReason.MaxStemCount, _morpher.MaxStemCount);
+ return Enumerable.Empty();
+ }
+ if (input.GetUnapplicationCount(_rule) >= _rule.MaxApplicationCount)
+ {
+ TraceFailure(-1, input, FailureReason.MaxApplicationCount, _rule.MaxApplicationCount);
+ return Enumerable.Empty();
+ }
+ if (!_outputFS.IsUnifiable(input.SyntacticFeatureStruct))
+ {
+ TraceFailure(-1, input, FailureReason.OutputSyntacticFeatureStruct, _outputFS);
return Enumerable.Empty();
}
@@ -61,14 +68,18 @@ public IEnumerable Apply(Word input)
for (int i = 0; i < _rules.Count; i++)
{
var srOutput = new List();
+ bool matched = false;
foreach (Word outWord in _rules[i].Apply(input))
{
+ matched = true;
+ bool rootFound = false;
// for computational complexity reasons, we ensure that the non-head is a root, otherwise we assume it is not
// a valid analysis and throw it away
foreach (
RootAllomorph allo in _morpher.SearchRootAllomorphs(_rule.Stratum, outWord.CurrentNonHead.Shape)
)
{
+ rootFound = true;
if (
!_rule.NonHeadRequiredSyntacticFeatureStruct.IsUnifiable(
((LexEntry)allo.Morpheme).SyntacticFeatureStruct,
@@ -76,6 +87,17 @@ RootAllomorph allo in _morpher.SearchRootAllomorphs(_rule.Stratum, outWord.Curre
)
)
{
+ if (_morpher.TraceManager.IsTracing)
+ {
+ Word tempInput = outWord.Clone();
+ tempInput.CurrentNonHead.RootAllomorph = allo;
+ TraceFailure(
+ i,
+ tempInput,
+ FailureReason.NonHeadRequiredSyntacticFeatureStruct,
+ _rule.NonHeadRequiredSyntacticFeatureStruct
+ );
+ }
continue;
}
// make sure any productivity restrictions on the stem are in the set the member has
@@ -90,9 +112,8 @@ RootAllomorph allo in _morpher.SearchRootAllomorphs(_rule.Stratum, outWord.Curre
Word tempInput = outWord.Clone();
tempInput.CurrentNonHead.RootAllomorph = allo;
tempInput.CurrentTrace = input.CurrentTrace;
- _morpher.TraceManager.CompoundingRuleNotUnapplied(
- _rule,
- -1,
+ TraceFailure(
+ i,
tempInput,
FailureReason.NonHeadProdRestrictMprFeatures,
((LexEntry)allo.Morpheme).MprFeatures
@@ -128,9 +149,10 @@ RootAllomorph allo in _morpher.SearchRootAllomorphs(_rule.Stratum, outWord.Curre
srOutput.Add(newWord);
}
}
+ if (!rootFound)
+ TraceFailure(i, outWord, FailureReason.NonHeadLexicalLookup, null);
}
- bool unapplied = false;
foreach (Word outWord in srOutput)
{
// the paths Out writes describe this rule's output, not the stem; see AnalysisAffixProcessRule
@@ -144,6 +166,12 @@ out FeatureStruct syntacticFS
)
)
{
+ TraceFailure(
+ i,
+ outWord,
+ FailureReason.HeadRequiredSyntacticFeatureStruct,
+ _rule.HeadRequiredSyntacticFeatureStruct
+ );
continue;
}
outWord.SyntacticFeatureStruct = syntacticFS;
@@ -155,14 +183,19 @@ out FeatureStruct syntacticFS
if (_morpher.TraceManager.IsTracing)
_morpher.TraceManager.MorphologicalRuleUnapplied(_rule, i, input, outWord);
output.Add(outWord);
- unapplied = true;
}
- if (_morpher.TraceManager.IsTracing && !unapplied)
- _morpher.TraceManager.MorphologicalRuleNotUnapplied(_rule, i, input);
+ if (!matched)
+ TraceFailure(i, input, FailureReason.Pattern, null);
}
return output;
}
+
+ private void TraceFailure(int index, Word input, FailureReason reason, object failureObj)
+ {
+ if (_morpher.TraceManager.IsTracing)
+ _morpher.TraceManager.CompoundingRuleRejected(_rule, index, input, reason, failureObj);
+ }
}
}
diff --git a/src/SIL.Machine.Morphology.HermitCrab/MorphologicalRules/AnalysisRealizationalAffixProcessRule.cs b/src/SIL.Machine.Morphology.HermitCrab/MorphologicalRules/AnalysisRealizationalAffixProcessRule.cs
index 89e30fbe2..404542b72 100644
--- a/src/SIL.Machine.Morphology.HermitCrab/MorphologicalRules/AnalysisRealizationalAffixProcessRule.cs
+++ b/src/SIL.Machine.Morphology.HermitCrab/MorphologicalRules/AnalysisRealizationalAffixProcessRule.cs
@@ -44,7 +44,17 @@ public IEnumerable Apply(Word input)
FeatureStruct realFS;
if (!_rule.RealizationalFeatureStruct.Unify(input.RealizationalFeatureStruct, out realFS))
+ {
+ if (_morpher.TraceManager.IsTracing)
+ _morpher.TraceManager.MorphologicalRuleNotUnapplied(
+ _rule,
+ -1,
+ input,
+ FailureReason.RealizationalFeatureStruct,
+ _rule.RealizationalFeatureStruct
+ );
return Enumerable.Empty();
+ }
var output = new List();
for (int i = 0; i < _rules.Count; i++)
@@ -64,7 +74,7 @@ public IEnumerable Apply(Word input)
}
if (_morpher.TraceManager.IsTracing && !unapplied)
- _morpher.TraceManager.MorphologicalRuleNotUnapplied(_rule, i, input);
+ _morpher.TraceManager.MorphologicalRuleNotUnapplied(_rule, i, input, FailureReason.Pattern, null);
}
return output;
}
diff --git a/src/SIL.Machine.Morphology.HermitCrab/SynthesisAffixTemplateRule.cs b/src/SIL.Machine.Morphology.HermitCrab/SynthesisAffixTemplateRule.cs
index e8cf7ee17..0b7156800 100644
--- a/src/SIL.Machine.Morphology.HermitCrab/SynthesisAffixTemplateRule.cs
+++ b/src/SIL.Machine.Morphology.HermitCrab/SynthesisAffixTemplateRule.cs
@@ -38,8 +38,18 @@ private void ApplySlots(Word input, int index, HashSet output)
{
for (int i = index; i < _rules.Count; i++)
{
+ bool producedOutput = false;
foreach (Word outWord in _rules[i].Apply(input))
+ {
+ producedOutput = true;
+ TraceSlot(input, outWord, i, TemplateSlotOutcome.RuleApplied);
ApplySlots(outWord, i + 1, output);
+ }
+
+ if (_template.Slots[i].Optional)
+ TraceSlot(input, null, i, TemplateSlotOutcome.OptionalSkipped);
+ else if (!producedOutput)
+ TraceSlot(input, null, i, TemplateSlotOutcome.RequiredUnfilled);
if (!_template.Slots[i].Optional)
{
@@ -53,5 +63,11 @@ private void ApplySlots(Word input, int index, HashSet output)
_morpher.TraceManager.EndApplyTemplate(_template, input, true);
output.Add(input);
}
+
+ private void TraceSlot(Word input, Word output, int index, TemplateSlotOutcome outcome)
+ {
+ if (_morpher.TraceManager.IsTracing)
+ _morpher.TraceManager.TemplateSlotProcessed(_template, index, input, output, false, outcome);
+ }
}
}
diff --git a/src/SIL.Machine.Morphology.HermitCrab/SynthesisStratumRule.cs b/src/SIL.Machine.Morphology.HermitCrab/SynthesisStratumRule.cs
index a15a6de5b..fc9a73491 100644
--- a/src/SIL.Machine.Morphology.HermitCrab/SynthesisStratumRule.cs
+++ b/src/SIL.Machine.Morphology.HermitCrab/SynthesisStratumRule.cs
@@ -71,7 +71,10 @@ public IEnumerable Apply(Word input)
mruleOutWord,
FailureReason.PartialParse,
null,
- null
+ new PartialParseFailure(
+ PartialParseCause.UnappliedMorphologicalRules,
+ mruleOutWord.NextMorphologicalRule
+ )
);
}
}
diff --git a/src/SIL.Machine.Morphology.HermitCrab/Trace.cs b/src/SIL.Machine.Morphology.HermitCrab/Trace.cs
index 10abc27ab..26eab5ccb 100644
--- a/src/SIL.Machine.Morphology.HermitCrab/Trace.cs
+++ b/src/SIL.Machine.Morphology.HermitCrab/Trace.cs
@@ -113,6 +113,8 @@ public enum TraceType
/// Parse failed trace
///
Failed,
+ TemplateSlotAnalysis,
+ TemplateSlotSynthesis,
}
///
@@ -152,5 +154,26 @@ public IHCRule Source
public Word Output { get; internal set; }
public FailureReason FailureReason { get; internal set; }
+
+ public object FailureObject { get; internal set; }
+
+ public Allomorph Allomorph { get; internal set; }
+
+ public LexEntry BlockingEntry { get; internal set; }
+
+ public PartialParseCause? PartialParseCause { get; internal set; }
+
+ public int? SlotIndex { get; internal set; }
+
+ public TemplateSlotOutcome? SlotOutcome { get; internal set; }
+
+ public IMorphologicalRule SlotRule { get; internal set; }
+
+ ///
+ /// Null until the lookup is fully enumerated; counts yielded Word candidates, before surface validation.
+ ///
+ public int? LexicalCandidateCount { get; internal set; }
+
+ public bool? IsLexicalGuess { get; internal set; }
}
}
diff --git a/src/SIL.Machine.Morphology.HermitCrab/TraceManager.cs b/src/SIL.Machine.Morphology.HermitCrab/TraceManager.cs
index 3fa99c9b9..72536a8a8 100644
--- a/src/SIL.Machine.Morphology.HermitCrab/TraceManager.cs
+++ b/src/SIL.Machine.Morphology.HermitCrab/TraceManager.cs
@@ -1,7 +1,9 @@
namespace SIL.Machine.Morphology.HermitCrab
{
- public class TraceManager : ITraceManager
+ public class TraceManager : IDetailedTraceManager
{
+ private readonly System.Runtime.CompilerServices.ConditionalWeakTable _lexicalLookups =
+ new System.Runtime.CompilerServices.ConditionalWeakTable();
public bool IsTracing { get; set; }
public object GenerateWords(Language lang)
@@ -82,9 +84,26 @@ public void MorphologicalRuleUnapplied(IMorphologicalRule rule, int subruleIndex
}
public void MorphologicalRuleNotUnapplied(IMorphologicalRule rule, int subruleIndex, Word input)
+ {
+ MorphologicalRuleNotUnapplied(rule, subruleIndex, input, FailureReason.None, null);
+ }
+
+ public void MorphologicalRuleNotUnapplied(
+ IMorphologicalRule rule,
+ int subruleIndex,
+ Word input,
+ FailureReason reason,
+ object failureObj
+ )
{
((Trace)input.CurrentTrace).Children.Add(
- new Trace(TraceType.MorphologicalRuleAnalysis, rule) { SubruleIndex = subruleIndex, Input = input }
+ new Trace(TraceType.MorphologicalRuleAnalysis, rule)
+ {
+ SubruleIndex = subruleIndex,
+ Input = input,
+ FailureReason = reason,
+ FailureObject = failureObj,
+ }
);
}
@@ -102,6 +121,7 @@ object failureObj
SubruleIndex = subruleIndex,
Input = input,
FailureReason = reason,
+ FailureObject = failureObj,
}
);
}
@@ -120,22 +140,37 @@ object failureObj
SubruleIndex = subruleIndex,
Input = input,
FailureReason = reason,
+ FailureObject = failureObj,
}
);
}
public void LexicalLookup(Stratum stratum, Word input)
{
- ((Trace)input.CurrentTrace).Children.Add(
- new Trace(TraceType.LexicalLookup, stratum) { Input = input.Clone() }
- );
+ var trace = new Trace(TraceType.LexicalLookup, stratum) { Input = input.Clone() };
+ ((Trace)input.CurrentTrace).Children.Add(trace);
+ _lexicalLookups.Remove(input);
+ _lexicalLookups.Add(input, trace);
+ }
+
+ public void LexicalLookupCompleted(Stratum stratum, Word input, int candidateCount, bool guessed)
+ {
+ if (_lexicalLookups.TryGetValue(input, out Trace trace))
+ {
+ trace.LexicalCandidateCount = candidateCount;
+ trace.IsLexicalGuess = guessed;
+ _lexicalLookups.Remove(input);
+ }
}
public void SynthesizeWord(Language lang, Word input)
{
var trace = new Trace(TraceType.WordSynthesis, lang) { Input = input.Clone() };
var curTrace = (Trace)input.CurrentTrace;
- curTrace.Children.Last.Children.Add(trace);
+ if (input.Source != null && _lexicalLookups.TryGetValue(input.Source, out Trace lookup))
+ lookup.Children.Add(trace);
+ else
+ curTrace.Children.Last.Children.Add(trace);
input.CurrentTrace = trace;
}
@@ -153,6 +188,7 @@ public void NonFinalTemplateAppliedLast(Stratum stratum, Word word)
{
Output = word,
FailureReason = FailureReason.PartialParse,
+ PartialParseCause = PartialParseCause.NonFinalTemplateAppliedLast,
}
);
}
@@ -164,6 +200,7 @@ public void ApplicableTemplatesNotApplied(Stratum stratum, Word word)
{
Output = word,
FailureReason = FailureReason.PartialParse,
+ PartialParseCause = PartialParseCause.ApplicableTemplatesNotApplied,
}
);
}
@@ -201,6 +238,7 @@ object failureObj
SubruleIndex = subruleIndex,
Input = input.Clone(),
FailureReason = reason,
+ FailureObject = failureObj,
}
);
}
@@ -245,13 +283,20 @@ object failureObj
SubruleIndex = subruleIndex,
Input = input,
FailureReason = reason,
+ FailureObject = failureObj,
}
);
}
public void Blocked(IHCRule rule, Word output)
{
- ((Trace)output.CurrentTrace).Children.Add(new Trace(TraceType.Blocked, rule) { Output = output });
+ ((Trace)output.CurrentTrace).Children.Add(
+ new Trace(TraceType.Blocked, rule)
+ {
+ Output = output,
+ BlockingEntry = output.RootAllomorph?.Morpheme as LexEntry,
+ }
+ );
}
public void Successful(Language lang, Word word)
@@ -262,7 +307,35 @@ public void Successful(Language lang, Word word)
public void Failed(Language lang, Word word, FailureReason reason, Allomorph allomorph, object failureObj)
{
((Trace)word.CurrentTrace).Children.Add(
- new Trace(TraceType.Failed, lang) { Output = word, FailureReason = reason }
+ new Trace(TraceType.Failed, lang)
+ {
+ Output = word,
+ FailureReason = reason,
+ FailureObject = failureObj,
+ Allomorph = allomorph,
+ PartialParseCause = (failureObj as PartialParseFailure)?.Cause,
+ }
+ );
+ }
+
+ public void TemplateSlotProcessed(
+ AffixTemplate template,
+ int slotIndex,
+ Word input,
+ Word output,
+ bool analysis,
+ TemplateSlotOutcome outcome
+ )
+ {
+ ((Trace)input.CurrentTrace).Children.Add(
+ new Trace(analysis ? TraceType.TemplateSlotAnalysis : TraceType.TemplateSlotSynthesis, template)
+ {
+ Input = input,
+ Output = output,
+ SlotIndex = slotIndex,
+ SlotOutcome = outcome,
+ SlotRule = output == null ? null : ((Trace)output.CurrentTrace).Source as IMorphologicalRule,
+ }
);
}
}
diff --git a/src/SIL.Machine.Morphology.HermitCrab/TraceManagerExtensions.cs b/src/SIL.Machine.Morphology.HermitCrab/TraceManagerExtensions.cs
new file mode 100644
index 000000000..62803e38c
--- /dev/null
+++ b/src/SIL.Machine.Morphology.HermitCrab/TraceManagerExtensions.cs
@@ -0,0 +1,65 @@
+namespace SIL.Machine.Morphology.HermitCrab
+{
+ internal static class TraceManagerExtensions
+ {
+ public static void MorphologicalRuleNotUnapplied(
+ this ITraceManager traceManager,
+ IMorphologicalRule rule,
+ int subruleIndex,
+ Word input,
+ FailureReason reason,
+ object failureObj
+ )
+ {
+ if (traceManager is IDetailedTraceManager detailed)
+ detailed.MorphologicalRuleNotUnapplied(rule, subruleIndex, input, reason, failureObj);
+ else
+ traceManager.MorphologicalRuleNotUnapplied(rule, subruleIndex, input);
+ }
+
+ ///
+ /// ITraceManager implementations previously received only the productivity-restriction rejection,
+ /// with no subrule index, so they keep exactly that and nothing more.
+ ///
+ public static void CompoundingRuleRejected(
+ this ITraceManager traceManager,
+ IMorphologicalRule rule,
+ int subruleIndex,
+ Word input,
+ FailureReason reason,
+ object failureObj
+ )
+ {
+ if (traceManager is IDetailedTraceManager)
+ traceManager.CompoundingRuleNotUnapplied(rule, subruleIndex, input, reason, failureObj);
+ else if (reason == FailureReason.NonHeadProdRestrictMprFeatures)
+ traceManager.CompoundingRuleNotUnapplied(rule, -1, input, reason, failureObj);
+ }
+
+ public static void LexicalLookupCompleted(
+ this ITraceManager traceManager,
+ Stratum stratum,
+ Word input,
+ int candidateCount,
+ bool guessed
+ )
+ {
+ if (traceManager is IDetailedTraceManager detailed)
+ detailed.LexicalLookupCompleted(stratum, input, candidateCount, guessed);
+ }
+
+ public static void TemplateSlotProcessed(
+ this ITraceManager traceManager,
+ AffixTemplate template,
+ int slotIndex,
+ Word input,
+ Word output,
+ bool analysis,
+ TemplateSlotOutcome outcome
+ )
+ {
+ if (traceManager is IDetailedTraceManager detailed)
+ detailed.TemplateSlotProcessed(template, slotIndex, input, output, analysis, outcome);
+ }
+ }
+}
diff --git a/src/SIL.Machine.Morphology.HermitCrab/Word.cs b/src/SIL.Machine.Morphology.HermitCrab/Word.cs
index 45d2642e6..8c9a2707e 100644
--- a/src/SIL.Machine.Morphology.HermitCrab/Word.cs
+++ b/src/SIL.Machine.Morphology.HermitCrab/Word.cs
@@ -249,6 +249,8 @@ public IEnumerable MorphologicalRules
internal int MorphologicalRuleApplicationCount => _mruleAppCount;
+ internal IMorphologicalRule NextMorphologicalRule => _mruleAppIndex < 0 ? null : _mruleApps[_mruleAppIndex];
+
internal bool IsAllMorphologicalRulesApplied
{
get { return _mruleAppIndex == -1; }
diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/SemanticCoverage/FieldworksProducibilityLedgerTests.cs b/tests/SIL.Machine.Morphology.HermitCrab.Tests/SemanticCoverage/FieldworksProducibilityLedgerTests.cs
index 8d159f5ce..595f43c9e 100644
--- a/tests/SIL.Machine.Morphology.HermitCrab.Tests/SemanticCoverage/FieldworksProducibilityLedgerTests.cs
+++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/SemanticCoverage/FieldworksProducibilityLedgerTests.cs
@@ -191,9 +191,9 @@ public void PinsTheHeadlineProducibleCounts()
TestContext.Out.WriteLine($"producible: Yes={yes} No={no} Conditional={conditional}");
- Assert.That(rows, Has.Count.EqualTo(88));
- Assert.That(yes, Is.EqualTo(62));
- Assert.That(no, Is.EqualTo(26));
+ Assert.That(rows, Has.Count.EqualTo(92));
+ Assert.That(yes, Is.EqualTo(65));
+ Assert.That(no, Is.EqualTo(27));
Assert.That(conditional, Is.EqualTo(0));
}
}
diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/TraceReasonTests.cs b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TraceReasonTests.cs
new file mode 100644
index 000000000..99e48291a
--- /dev/null
+++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/TraceReasonTests.cs
@@ -0,0 +1,628 @@
+using NUnit.Framework;
+using SIL.Machine.Annotations;
+using SIL.Machine.FeatureModel;
+using SIL.Machine.Matching;
+using SIL.Machine.Morphology.HermitCrab.MorphologicalRules;
+using SIL.Machine.Morphology.HermitCrab.PhonologicalRules;
+using SIL.Machine.Rules;
+
+namespace SIL.Machine.Morphology.HermitCrab;
+
+[TestFixture]
+public class TraceReasonTests
+{
+ private Language _language = default!;
+ private Stratum _stratum = default!;
+ private CharacterDefinitionTable _table = default!;
+ private TraceManager _traceManager = default!;
+ private static readonly FeatureStruct Any = FeatureStruct.New().Symbol(HCFeatureSystem.Segment).Value;
+
+ [SetUp]
+ public void SetUp()
+ {
+ var phonology = new FeatureSystem
+ {
+ new SymbolicFeature("letter", "abds".Select(c => new FeatureSymbol(c.ToString()))),
+ };
+ _table = new CharacterDefinitionTable();
+ foreach (char c in "abds")
+ _table.AddSegment(c.ToString(), FeatureStruct.New(phonology).Symbol(c.ToString()).Value.Clone());
+ _table.AddBoundary("+");
+ _stratum = new Stratum(_table) { Name = "lexical" };
+ _language = new Language { PhonologicalFeatureSystem = phonology, Strata = { _stratum } };
+ _language.SyntacticFeatureSystem.AddPartsOfSpeech(new FeatureSymbol("N"), new FeatureSymbol("V"));
+ _traceManager = new TraceManager { IsTracing = true };
+ }
+
+ [TestCase("a", "Pattern")]
+ [TestCase("as", "MaxApplicationCount")]
+ public void AffixAnalysis_ReportsFailedGate(string form, string expectedReason)
+ {
+ AffixProcessRule rule = Suffix();
+ Word input = AnalysisInput(form);
+ if (expectedReason == "MaxApplicationCount")
+ input.MorphologicalRuleUnapplied(rule);
+ input.Freeze();
+ Assert.That(rule.CompileAnalysisRule(Morpher()).Apply(input), Is.Empty);
+ Trace failure = Events(input).Single(t => t.Type == TraceType.MorphologicalRuleAnalysis);
+ Assert.That(failure.FailureReason.ToString(), Is.EqualTo(expectedReason));
+ Assert.That(failure.SubruleIndex, Is.EqualTo(expectedReason == "Pattern" ? 0 : -1));
+ }
+
+ [Test]
+ public void AffixAnalysis_ReportsOutputFeatureConflict()
+ {
+ AffixProcessRule rule = Suffix();
+ rule.OutSyntacticFeatureStruct = Pos("V");
+ Word input = AnalysisInput("as");
+ input.SyntacticFeatureStruct = Pos("N");
+ input.Freeze();
+ Assert.That(rule.CompileAnalysisRule(Morpher()).Apply(input), Is.Empty);
+ Trace failure = Events(input).Single(t => t.Type == TraceType.MorphologicalRuleAnalysis);
+ Assert.That(failure.FailureReason.ToString(), Is.EqualTo("OutputSyntacticFeatureStruct"));
+ Assert.That(failure.FailureObject, Is.InstanceOf());
+ }
+
+ [Test]
+ public void RealizationalAnalysis_ReportsFeatureConflict()
+ {
+ var rule = new RealizationalAffixProcessRule { RealizationalFeatureStruct = Pos("V") };
+ rule.Allomorphs.Add(Suffix().Allomorphs[0]);
+ Word input = AnalysisInput("as");
+ input.RealizationalFeatureStruct = Pos("N");
+ input.Freeze();
+ Assert.That(rule.CompileAnalysisRule(Morpher()).Apply(input), Is.Empty);
+ Trace failure = Events(input).Single(t => t.Type == TraceType.MorphologicalRuleAnalysis);
+ Assert.That(failure.FailureReason.ToString(), Is.EqualTo("RealizationalFeatureStruct"));
+ Assert.That(failure.FailureObject, Is.SameAs(rule.RealizationalFeatureStruct));
+ }
+
+ [TestCase(1, true, "a", "OptionalSkipped")]
+ [TestCase(2, true, "a", "OptionalSkipped")]
+ [TestCase(1, false, "a", "RequiredUnfilled")]
+ [TestCase(2, false, "a", "RequiredUnfilled")]
+ [TestCase(1, false, "as", "RuleApplied")]
+ [TestCase(2, false, "as", "RuleApplied")]
+ public void TemplateAnalysis_ReportsSlotOutcome(int parallelism, bool optional, string form, string outcome)
+ {
+ var template = new AffixTemplate { Slots = { new AffixTemplateSlot(Suffix()) { Optional = optional } } };
+ Word input = AnalysisInput(form);
+ input.Freeze();
+ Word[] output = template.CompileAnalysisRule(Morpher(parallelism)).Apply(input).ToArray();
+ Assert.That(output.Length, Is.EqualTo(outcome == "RequiredUnfilled" ? 0 : 1));
+ Trace slot = Events(input).Single(t => t.Type.ToString() == "TemplateSlotAnalysis");
+ Assert.That(slot.SlotIndex, Is.EqualTo(0));
+ Assert.That(slot.SlotOutcome, Is.EqualTo(Enum.Parse(outcome)));
+ Assert.That(slot.Output is null, Is.EqualTo(outcome != "RuleApplied"));
+ }
+
+ [TestCase(true, "OptionalSkipped")]
+ [TestCase(false, "RequiredUnfilled")]
+ public void TemplateSynthesis_ReportsEmptySlot(bool optional, string outcome)
+ {
+ var template = new AffixTemplate { Slots = { new AffixTemplateSlot(Suffix()) { Optional = optional } } };
+ Word input = new Word(Entry("a").PrimaryAllomorph, new FeatureStruct());
+ _traceManager.AnalyzeWord(_language, input);
+ input.Freeze();
+ Word[] output = template.CompileSynthesisRule(Morpher()).Apply(input).ToArray();
+ Assert.That(output.Length, Is.EqualTo(optional ? 1 : 0));
+ Trace slot = Events(input).Single(t => t.Type.ToString() == "TemplateSlotSynthesis");
+ Assert.That(slot.SlotOutcome, Is.EqualTo(Enum.Parse(outcome)));
+ }
+
+ [TestCase("a", 1)]
+ [TestCase("b", 0)]
+ public void LexicalLookup_ReportsCompletedCandidateCount(string form, int count)
+ {
+ Entry("a");
+ Word[] output = Morpher().ParseWord(form, out object trace).ToArray();
+ Assert.That(output.Length, Is.EqualTo(count));
+ Trace lookup = Descendants((Trace)trace).Single(t => t.Type == TraceType.LexicalLookup);
+ Assert.That(lookup.LexicalCandidateCount, Is.EqualTo(count));
+ Assert.That(lookup.IsLexicalGuess, Is.EqualTo(false));
+ Assert.That(lookup.Children.Count(t => t.Type == TraceType.WordSynthesis), Is.EqualTo(count));
+ }
+
+ [Test]
+ public void Blocking_IdentifiesReplacementFamilyEntry()
+ {
+ LexEntry stem = Entry("a");
+ LexEntry blocker = Entry("b");
+ var family = new LexFamily { Entries = { stem, blocker } };
+ _language.Families.Add(family);
+ AffixProcessRule suffix = Suffix();
+ suffix.Blockable = true;
+ Word input = new Word(stem.PrimaryAllomorph, new FeatureStruct());
+ input.MorphologicalRuleUnapplied(suffix);
+ _traceManager.AnalyzeWord(_language, input);
+ input.Freeze();
+ Word result = suffix.CompileSynthesisRule(Morpher()).Apply(input).Single();
+ Assert.That(result.RootAllomorph.Morpheme, Is.SameAs(blocker));
+ Trace blocked = Events(input).Single(t => t.Type == TraceType.Blocked);
+ Assert.That(blocked.BlockingEntry, Is.SameAs(blocker));
+ }
+
+ [Test]
+ public void EnvironmentFailure_RetainsAllomorphAndFailedAlternatives()
+ {
+ RootAllomorph allomorph = Entry("a").PrimaryAllomorph;
+ var first = new AllomorphEnvironment(
+ ConstraintType.Require,
+ null,
+ Pattern.New().Annotation(_table["b"].FeatureStruct).Value
+ )
+ {
+ Name = "before b",
+ };
+ var second = new AllomorphEnvironment(
+ ConstraintType.Require,
+ Pattern.New().Annotation(_table["d"].FeatureStruct).Value,
+ null
+ )
+ {
+ Name = "after d",
+ };
+ allomorph.Environments.Add(first);
+ allomorph.Environments.Add(second);
+ Assert.That(Morpher().ParseWord("a", out object trace), Is.Empty);
+ Trace failure = Descendants((Trace)trace).Single(t => t.FailureReason == FailureReason.Environments);
+ Assert.That(failure.Allomorph, Is.SameAs(allomorph));
+ Assert.That(failure.FailureObject, Is.EquivalentTo(new[] { first, second }));
+ }
+
+ [Test]
+ public void CompoundingAnalysis_ReportsPatternRejection()
+ {
+ CompoundingRule rule = Compound();
+ Word input = AnalysisInput("a");
+ input.Freeze();
+ Assert.That(rule.CompileAnalysisRule(Morpher()).Apply(input), Is.Empty);
+ Trace failure = Events(input).Single(t => t.Type == TraceType.CompoundingRuleAnalysis);
+ Assert.That(failure.FailureReason, Is.EqualTo(FailureReason.Pattern));
+ Assert.That(failure.SubruleIndex, Is.Zero);
+ }
+
+ [Test]
+ public void PhonologicalAnalysis_NoUnapplicationKeepsReasonUnknown()
+ {
+ var rule = new RewriteRule
+ {
+ Lhs = Pattern.New().Annotation(_table["b"].FeatureStruct).Value,
+ Subrules =
+ {
+ new RewriteSubrule { Rhs = Pattern.New().Annotation(_table["d"].FeatureStruct).Value },
+ },
+ };
+ Word input = AnalysisInput("a");
+ Assert.That(rule.CompileAnalysisRule(Morpher()).Apply(input), Is.Empty);
+ Trace failure = Events(input).Single(t => t.Type == TraceType.PhonologicalRuleAnalysis);
+ Assert.Multiple(() =>
+ {
+ Assert.That(failure.Source, Is.SameAs(rule));
+ Assert.That(failure.SubruleIndex, Is.Zero);
+ Assert.That(failure.Input.Shape.ToString(_table, false), Is.EqualTo("a"));
+ Assert.That(failure.Output, Is.Null);
+ Assert.That(failure.FailureReason, Is.EqualTo(FailureReason.None));
+ });
+ }
+
+ [TestCase(false, "UnappliedMorphologicalRules")]
+ [TestCase(true, "RealizationalFeatureMismatch")]
+ public void PartialParse_ReportsCompletionGate(bool realizational, string cause)
+ {
+ Entry("a");
+ IMorphologicalRule rule;
+ if (realizational)
+ {
+ var affix = new RealizationalAffixProcessRule { RealizationalFeatureStruct = Pos("V") };
+ affix.Allomorphs.Add(Suffix().Allomorphs[0]);
+ rule = affix;
+ }
+ else
+ {
+ rule = Suffix();
+ }
+ _stratum.MorphologicalRules.Add(rule);
+ bool synthesis = false;
+ Morpher morpher = Morpher();
+ morpher.LexEntrySelector = _ =>
+ {
+ synthesis = true;
+ return true;
+ };
+ morpher.RuleSelector = selected => !synthesis;
+ Assert.That(morpher.ParseWord("as", out object trace), Is.Empty);
+ Trace failure = Descendants((Trace)trace).Single(t => t.FailureReason == FailureReason.PartialParse);
+ Assert.That(failure.PartialParseCause, Is.EqualTo(Enum.Parse(cause)));
+ }
+
+ [TestCase(false, "ApplicableTemplatesNotApplied")]
+ [TestCase(true, "NonFinalTemplateAppliedLast")]
+ public void PartialParse_ReportsTemplateCompletionGate(bool nonFinal, string cause)
+ {
+ Entry("a");
+ var template = new AffixTemplate
+ {
+ IsFinal = !nonFinal,
+ Slots = { new AffixTemplateSlot(Suffix()) { Optional = nonFinal } },
+ };
+ _stratum.AffixTemplates.Add(template);
+ Assert.That(Morpher().ParseWord("a", out object trace), Is.Empty);
+ Trace failure = Descendants((Trace)trace).Single(t => t.FailureReason == FailureReason.PartialParse);
+ Assert.That(failure.PartialParseCause, Is.EqualTo(Enum.Parse(cause)));
+ }
+
+ [TestCase(1)]
+ [TestCase(2)]
+ public void OptionalSlot_ReportsAppliedAndSkippedBranches(int parallelism)
+ {
+ var suffix = Suffix();
+ var template = new AffixTemplate { Slots = { new AffixTemplateSlot(suffix) { Optional = true } } };
+ Word input = AnalysisInput("as");
+ input.Freeze();
+ Word[] output = template.CompileAnalysisRule(Morpher(parallelism)).Apply(input).ToArray();
+ Assert.That(output.Select(w => w.Shape.ToString(_table, false)), Is.EquivalentTo(new[] { "a", "as" }));
+ Trace[] slots = Events(input).Where(t => t.Type == TraceType.TemplateSlotAnalysis).ToArray();
+ Assert.That(
+ slots.Select(t => t.SlotOutcome),
+ Is.EquivalentTo(new[] { TemplateSlotOutcome.RuleApplied, TemplateSlotOutcome.OptionalSkipped })
+ );
+ Assert.That(slots.Single(t => t.SlotOutcome == TemplateSlotOutcome.RuleApplied).SlotRule, Is.SameAs(suffix));
+ }
+
+ [Test]
+ public void LexicalLookup_InterleavedInputsKeepTheirOwnSynthesisAndCompletion()
+ {
+ Word first = AnalysisInput("a");
+ Word second = new Word(_stratum, _table.Segment("b")) { CurrentTrace = first.CurrentTrace };
+ _traceManager.LexicalLookup(_stratum, first);
+ _traceManager.LexicalLookup(_stratum, second);
+ Word synthesis = first.Clone();
+ _traceManager.SynthesizeWord(_language, synthesis);
+ _traceManager.LexicalLookupCompleted(_stratum, second, 0, false);
+ _traceManager.LexicalLookupCompleted(_stratum, first, 1, false);
+ Trace[] lookups = Events(first).Where(t => t.Type == TraceType.LexicalLookup).ToArray();
+ Assert.Multiple(() =>
+ {
+ Assert.That(lookups.Select(t => t.LexicalCandidateCount), Is.EqualTo(new[] { 1, 0 }));
+ Assert.That(lookups[0].Children.Single().Type, Is.EqualTo(TraceType.WordSynthesis));
+ Assert.That(lookups[1].Children, Is.Empty);
+ });
+ }
+
+ [Test]
+ public void LegacyTraceManager_ReceivesExistingCallbacks()
+ {
+ ITraceManager legacy = System.Reflection.DispatchProxy.Create();
+ var proxy = (LegacyTraceProxy)legacy;
+ proxy.Target = _traceManager;
+ Word input = AnalysisInput("a");
+ var morpher = new Morpher(legacy, _language, 1);
+ Assert.That(Suffix().CompileAnalysisRule(morpher).Apply(input), Is.Empty);
+ Assert.That(
+ proxy.Calls.Count(name => name == nameof(ITraceManager.MorphologicalRuleNotUnapplied)),
+ Is.EqualTo(1)
+ );
+ var template = new AffixTemplate { Slots = { new AffixTemplateSlot(Suffix()) { Optional = true } } };
+ Assert.That(template.CompileAnalysisRule(morpher).Apply(input).Count(), Is.EqualTo(1));
+ Assert.That(Events(input).Any(t => t.Type == TraceType.TemplateSlotAnalysis), Is.False);
+ Assert.That(morpher.ParseWord("b", out object trace), Is.Empty);
+ Assert.That(
+ Descendants((Trace)trace).Single(t => t.Type == TraceType.LexicalLookup).LexicalCandidateCount,
+ Is.Null
+ );
+ }
+
+ [TestCase("MaxStemCount")]
+ [TestCase("NonHeadLexicalLookup")]
+ [TestCase("NonHeadRequiredSyntacticFeatureStruct")]
+ [TestCase("NonHeadProdRestrictMprFeatures")]
+ public void LegacyTraceManager_ReceivesOnlyTheExistingCompoundingRejection(string reason)
+ {
+ ITraceManager legacy = System.Reflection.DispatchProxy.Create();
+ var proxy = (LegacyTraceProxy)legacy;
+ proxy.Target = _traceManager;
+ CompoundingRule rule = Compound();
+ if (reason != "NonHeadLexicalLookup")
+ Entry("b");
+ if (reason == "NonHeadRequiredSyntacticFeatureStruct")
+ rule.NonHeadRequiredSyntacticFeatureStruct = Pos("V");
+ else if (reason == "NonHeadProdRestrictMprFeatures")
+ rule.NonHeadProdRestrictionsMprFeatures.Add(new MprFeature { Name = "required" });
+ // the morpher indexes root allomorphs when constructed, so it is built after the entries
+ var morpher = new Morpher(legacy, _language, 1);
+ Morpher detailedMorpher = Morpher();
+ if (reason == "MaxStemCount")
+ morpher.MaxStemCount = detailedMorpher.MaxStemCount = 1;
+ Word input = AnalysisInput("ab");
+ input.Freeze();
+ Word detailedInput = AnalysisInput("ab");
+ detailedInput.Freeze();
+
+ Assert.That(rule.CompileAnalysisRule(morpher).Apply(input), Is.Empty);
+ Assert.That(rule.CompileAnalysisRule(detailedMorpher).Apply(detailedInput), Is.Empty);
+
+ Assert.That(
+ Events(detailedInput).Single(t => t.Type == TraceType.CompoundingRuleAnalysis).FailureReason,
+ Is.EqualTo(Enum.Parse(reason))
+ );
+ Trace[] failures = Events(input).Where(t => t.Type == TraceType.CompoundingRuleAnalysis).ToArray();
+ if (reason == "NonHeadProdRestrictMprFeatures")
+ {
+ Assert.That(failures.Single().FailureReason, Is.EqualTo(FailureReason.NonHeadProdRestrictMprFeatures));
+ Assert.That(failures.Single().SubruleIndex, Is.EqualTo(-1));
+ }
+ else
+ {
+ Assert.That(failures, Is.Empty);
+ Assert.That(proxy.Calls, Does.Not.Contain(nameof(ITraceManager.CompoundingRuleNotUnapplied)));
+ }
+ }
+
+ public class LegacyTraceProxy : System.Reflection.DispatchProxy
+ {
+ public ITraceManager Target { get; set; } = default!;
+ public List Calls { get; } = new List();
+
+ protected override object? Invoke(System.Reflection.MethodInfo? targetMethod, object?[]? args)
+ {
+ Calls.Add(targetMethod!.Name);
+ return targetMethod.Invoke(Target, args);
+ }
+ }
+
+ [TestCase("a", 1)]
+ [TestCase("b", 0)]
+ public void LexicalGuess_ReportsSeparateCompletedLookup(string form, int count)
+ {
+ _table.AddNaturalClass(new NaturalClass(_table["a"].FeatureStruct) { Name = "A" });
+ _stratum.Entries.Add(
+ new LexEntry
+ {
+ Id = "pattern",
+ SyntacticFeatureStruct = Pos("N"),
+ Allomorphs = { new RootAllomorph(new Segments(_table, "[A]*", true)) },
+ }
+ );
+ Assert.That(Morpher().ParseWord(form, out object trace, true).Count(), Is.EqualTo(count));
+ Trace[] lookups = Descendants((Trace)trace).Where(t => t.Type == TraceType.LexicalLookup).ToArray();
+ Assert.That(lookups.Select(t => t.IsLexicalGuess), Is.EqualTo(new[] { false, true }));
+ Assert.That(lookups.Select(t => t.LexicalCandidateCount), Is.EqualTo(new[] { 0, count }));
+ Assert.That(lookups[1].Children.Count(t => t.Type == TraceType.WordSynthesis), Is.EqualTo(count));
+ }
+
+ [Test]
+ public void InterruptedLookup_DoesNotReportCompletedCount()
+ {
+ Entry("a").Allomorphs.Add(new RootAllomorph(new Segments(_table, "b")));
+ Morpher morpher = Morpher();
+ morpher.MaxAlternatives = 1;
+ object trace = null!;
+ Assert.Throws(() => morpher.ParseWord("a", out trace).ToArray());
+ Assert.That(
+ Descendants((Trace)trace).Single(t => t.Type == TraceType.LexicalLookup).LexicalCandidateCount,
+ Is.Null
+ );
+ }
+
+ [TestCase("MaxStemCount")]
+ [TestCase("MaxApplicationCount")]
+ [TestCase("OutputSyntacticFeatureStruct")]
+ public void CompoundingAnalysis_ReportsRuleGate(string reason)
+ {
+ CompoundingRule rule = Compound();
+ Word input = AnalysisInput("ab");
+ Morpher morpher = Morpher();
+ switch (reason)
+ {
+ case "MaxStemCount":
+ morpher.MaxStemCount = 1;
+ break;
+ case "MaxApplicationCount":
+ input.MorphologicalRuleUnapplied(rule);
+ break;
+ default:
+ input.SyntacticFeatureStruct = Pos("N");
+ rule.OutSyntacticFeatureStruct = Pos("V");
+ break;
+ }
+ input.Freeze();
+ Assert.That(rule.CompileAnalysisRule(morpher).Apply(input), Is.Empty);
+ Trace failure = Events(input).Single(t => t.Type == TraceType.CompoundingRuleAnalysis);
+ Assert.That(failure.FailureReason, Is.EqualTo(Enum.Parse(reason)));
+ Assert.That(failure.SubruleIndex, Is.EqualTo(-1));
+ Assert.That(failure.FailureObject, Is.Not.Null);
+ }
+
+ [TestCase("NonHeadLexicalLookup")]
+ [TestCase("NonHeadRequiredSyntacticFeatureStruct")]
+ [TestCase("NonHeadProdRestrictMprFeatures")]
+ public void CompoundingAnalysis_ReportsNonHeadRejection(string reason)
+ {
+ CompoundingRule rule = Compound();
+ LexEntry? nonHead = reason == "NonHeadLexicalLookup" ? null : Entry("b");
+ if (reason == "NonHeadRequiredSyntacticFeatureStruct")
+ rule.NonHeadRequiredSyntacticFeatureStruct = Pos("V");
+ else if (reason == "NonHeadProdRestrictMprFeatures")
+ rule.NonHeadProdRestrictionsMprFeatures.Add(new MprFeature { Name = "required" });
+ Word input = AnalysisInput("ab");
+ input.Freeze();
+ Assert.That(rule.CompileAnalysisRule(Morpher()).Apply(input), Is.Empty);
+ Trace failure = Events(input).Single(t => t.Type == TraceType.CompoundingRuleAnalysis);
+ Assert.That(failure.FailureReason, Is.EqualTo(Enum.Parse(reason)));
+ Assert.That(failure.SubruleIndex, Is.Zero);
+ if (nonHead != null)
+ Assert.That(failure.Input.CurrentNonHead.RootAllomorph.Morpheme, Is.SameAs(nonHead));
+ if (reason == "NonHeadProdRestrictMprFeatures")
+ Assert.That(failure.FailureObject, Is.SameAs(nonHead!.MprFeatures));
+ }
+
+ [Test]
+ public void CompoundingAnalysis_SuccessRetainsExistingTraceType()
+ {
+ CompoundingRule rule = Compound();
+ Entry("b");
+ Word input = AnalysisInput("ab");
+ input.Freeze();
+ Word output = rule.CompileAnalysisRule(Morpher()).Apply(input).Single();
+ Assert.That(output.Shape.ToString(_table, false), Is.EqualTo("a"));
+ Trace applied = Events(input).Single();
+ Assert.That(applied.Type, Is.EqualTo(TraceType.MorphologicalRuleAnalysis));
+ Assert.That(applied.Source, Is.SameAs(rule));
+ Assert.That(applied.Output, Is.SameAs(output));
+ }
+
+ [TestCase(1, false)]
+ [TestCase(1, true)]
+ [TestCase(2, false)]
+ [TestCase(2, true)]
+ public void TraceDetails_PreserveMorphologicalAnalyses(int parallelism, bool tracing)
+ {
+ Entry("a");
+ AffixProcessRule suffix = Suffix();
+ _stratum.AffixTemplates.Add(
+ new AffixTemplate { Slots = { new AffixTemplateSlot(suffix) { Optional = true } } }
+ );
+ _traceManager.IsTracing = tracing;
+ Morpher morpher = Morpher(parallelism);
+ Word result = morpher.ParseWord("as").Single();
+ Assert.That(
+ result.AllomorphsInMorphOrder.Select(a => a.Morpheme),
+ Is.EqualTo(new Morpheme[] { _stratum.Entries.Single(), suffix })
+ );
+ Assert.That(morpher.ParseWord("a").Single().AllomorphsInMorphOrder.Count(), Is.EqualTo(1));
+ Assert.That(morpher.ParseWord("bs"), Is.Empty);
+ }
+
+ [TestCase(false)]
+ [TestCase(true)]
+ public void TemplateSynthesis_ReportsAppliedRuleWithoutFalseRequiredFailure(bool optional)
+ {
+ AffixProcessRule suffix = Suffix();
+ var template = new AffixTemplate { Slots = { new AffixTemplateSlot(suffix) { Optional = optional } } };
+ Word input = new Word(Entry("a").PrimaryAllomorph, new FeatureStruct());
+ input.MorphologicalRuleUnapplied(suffix);
+ _traceManager.AnalyzeWord(_language, input);
+ input.Freeze();
+ Word[] output = template.CompileSynthesisRule(Morpher()).Apply(input).ToArray();
+ Assert.That(output.Length, Is.EqualTo(optional ? 2 : 1));
+ Trace[] slots = Events(input).Where(t => t.Type == TraceType.TemplateSlotSynthesis).ToArray();
+ Assert.That(slots.Length, Is.EqualTo(optional ? 2 : 1));
+ Assert.That(slots.Any(t => t.SlotOutcome == TemplateSlotOutcome.RequiredUnfilled), Is.False);
+ Assert.That(slots.Single(t => t.SlotOutcome == TemplateSlotOutcome.RuleApplied).SlotRule, Is.SameAs(suffix));
+ }
+
+ [Test]
+ public void PartialParse_StratumReportsPendingRule()
+ {
+ Entry("a");
+ AffixProcessRule suffix = Suffix();
+ suffix.RequiredStemName = new StemName(Pos("N")) { Name = "stem" };
+ _stratum.MorphologicalRules.Add(suffix);
+ Assert.That(Morpher().ParseWord("as", out object trace), Is.Empty);
+ Trace failure = Descendants((Trace)trace).Single(t => t.FailureReason == FailureReason.PartialParse);
+ Assert.That(failure.PartialParseCause, Is.EqualTo(PartialParseCause.UnappliedMorphologicalRules));
+ Assert.That(((PartialParseFailure)failure.FailureObject).Rule, Is.SameAs(suffix));
+ }
+
+ [Test]
+ public void PartialParse_FeatureConflictTakesPrecedenceOverPendingRule()
+ {
+ Entry("a");
+ var realizational = new RealizationalAffixProcessRule { RealizationalFeatureStruct = Pos("V") };
+ realizational.Allomorphs.Add(Suffix().Allomorphs[0]);
+ AffixProcessRule suffix = Suffix();
+ suffix.Allomorphs[0].Rhs[1] = new InsertSegments(_table, "+d");
+ _stratum.MorphologicalRules.Add(realizational);
+ _stratum.MorphologicalRules.Add(suffix);
+ bool synthesis = false;
+ Morpher morpher = Morpher();
+ morpher.LexEntrySelector = _ =>
+ {
+ synthesis = true;
+ return true;
+ };
+ morpher.RuleSelector = _ => !synthesis;
+ Assert.That(morpher.ParseWord("asd", out object trace), Is.Empty);
+ Trace failure = Descendants((Trace)trace).Single(t => t.FailureReason == FailureReason.PartialParse);
+ Assert.Multiple(() =>
+ {
+ Assert.That(failure.Output.IsAllMorphologicalRulesApplied, Is.False);
+ Assert.That(failure.PartialParseCause, Is.EqualTo(PartialParseCause.RealizationalFeatureMismatch));
+ Assert.That(((PartialParseFailure)failure.FailureObject).Rule, Is.Null);
+ });
+ }
+
+ [Test]
+ public void LexicalLookup_CountsYieldedAllomorphCandidates()
+ {
+ Entry("a").Allomorphs.Add(new RootAllomorph(new Segments(_table, "b")));
+ Assert.That(Morpher().ParseWord("a", out object trace).Count(), Is.EqualTo(1));
+ Trace lookup = Descendants((Trace)trace).Single(t => t.Type == TraceType.LexicalLookup);
+ Assert.That(lookup.LexicalCandidateCount, Is.EqualTo(2));
+ Assert.That(lookup.Children.Count(t => t.Type == TraceType.WordSynthesis), Is.EqualTo(2));
+ }
+
+ private Morpher Morpher(int parallelism = 1) => new Morpher(_traceManager, _language, parallelism);
+
+ private FeatureStruct Pos(string id) => FeatureStruct.New(_language.SyntacticFeatureSystem).Symbol(id).Value;
+
+ private Word AnalysisInput(string form)
+ {
+ var word = new Word(_stratum, _table.Segment(form));
+ _traceManager.AnalyzeWord(_language, word);
+ return word;
+ }
+
+ private LexEntry Entry(string form)
+ {
+ var entry = new LexEntry
+ {
+ Id = form,
+ SyntacticFeatureStruct = Pos("N"),
+ Allomorphs = { new RootAllomorph(new Segments(_table, form)) },
+ };
+ _stratum.Entries.Add(entry);
+ return entry;
+ }
+
+ private AffixProcessRule Suffix()
+ {
+ var rule = new AffixProcessRule { Id = "suffix", Gloss = "suffix" };
+ rule.Allomorphs.Add(
+ new AffixProcessAllomorph
+ {
+ Lhs = { Pattern.New("stem").Annotation(Any).OneOrMore.Value },
+ Rhs = { new CopyFromInput("stem"), new InsertSegments(_table, "+s") },
+ }
+ );
+ return rule;
+ }
+
+ private CompoundingRule Compound()
+ {
+ var rule = new CompoundingRule { Name = "compound" };
+ rule.Subrules.Add(
+ new CompoundingSubrule
+ {
+ HeadLhs = { Pattern.New("head").Annotation(Any).OneOrMore.Value },
+ NonHeadLhs = { Pattern.New("nonhead").Annotation(Any).OneOrMore.Value },
+ Rhs = { new CopyFromInput("head"), new InsertSegments(_table, "+"), new CopyFromInput("nonhead") },
+ }
+ );
+ _stratum.MorphologicalRules.Add(rule);
+ return rule;
+ }
+
+ private static IEnumerable Events(Word word) => Descendants((Trace)word.CurrentTrace);
+
+ private static IEnumerable Descendants(Trace trace)
+ {
+ foreach (Trace child in trace.Children)
+ {
+ yield return child;
+ foreach (Trace descendant in Descendants(child))
+ yield return descendant;
+ }
+ }
+}