diff --git a/src/SIL.Machine.Morphology.HermitCrab.Tool/HCContext.cs b/src/SIL.Machine.Morphology.HermitCrab.Tool/HCContext.cs index e6c8866c0..c6ec83a66 100644 --- a/src/SIL.Machine.Morphology.HermitCrab.Tool/HCContext.cs +++ b/src/SIL.Machine.Morphology.HermitCrab.Tool/HCContext.cs @@ -10,7 +10,7 @@ internal class HCContext(Language language, TextWriter outWriter) public void Compile() { - _morpher = new Morpher(new TraceManager(), _language); + _morpher = new Morpher(new TraceManager { RetainWords = false }, _language); } public Language Language diff --git a/src/SIL.Machine.Morphology.HermitCrab.Tool/ParseCommand.cs b/src/SIL.Machine.Morphology.HermitCrab.Tool/ParseCommand.cs index 86bfc0db1..a1b1d7a4c 100644 --- a/src/SIL.Machine.Morphology.HermitCrab.Tool/ParseCommand.cs +++ b/src/SIL.Machine.Morphology.HermitCrab.Tool/ParseCommand.cs @@ -74,28 +74,19 @@ private void PrintTrace(Trace trace, int indent, HashSet lineIndices) _context.Out.Write("{0}: {1}", ruleLabel, trace.Source.Name); first = false; } - bool analysis = IsAnalysis(trace.Type); - if (trace.Input != null) + if (trace.InputText != null) { if (!first) _context.Out.Write(", "); first = false; - CharacterDefinitionTable table = trace.Input.Stratum.CharacterDefinitionTable; - _context.Out.Write( - "Input: {0}", - analysis ? trace.Input.Shape.ToRegexString(table, true) : trace.Input.Shape.ToString(table, true) - ); + _context.Out.Write("Input: {0}", trace.InputText); } - if (trace.Output != null) + if (trace.OutputText != null) { if (!first) _context.Out.Write(", "); first = false; - CharacterDefinitionTable table = trace.Output.Stratum.CharacterDefinitionTable; - _context.Out.Write( - "Output: {0}", - analysis ? trace.Output.Shape.ToRegexString(table, true) : trace.Output.Shape.ToString(table, true) - ); + _context.Out.Write("Output: {0}", trace.OutputText); } if (trace.FailureReason != FailureReason.None) { @@ -130,36 +121,6 @@ private void PrintIndent(int indent, HashSet lineIndices) _context.Out.Write(lineIndices.Contains(i) ? "|" : " "); } - private static bool IsAnalysis(TraceType type) - { - switch (type) - { - case TraceType.WordAnalysis: - case TraceType.LexicalLookup: - case TraceType.StratumAnalysisInput: - case TraceType.StratumAnalysisOutput: - case TraceType.TemplateAnalysisInput: - case TraceType.TemplateAnalysisOutput: - case TraceType.MorphologicalRuleAnalysis: - case TraceType.PhonologicalRuleAnalysis: - return true; - - case TraceType.WordSynthesis: - case TraceType.Successful: - case TraceType.Failed: - case TraceType.Blocked: - case TraceType.StratumSynthesisInput: - case TraceType.StratumSynthesisOutput: - case TraceType.TemplateSynthesisInput: - case TraceType.TemplateSynthesisOutput: - case TraceType.MorphologicalRuleSynthesis: - case TraceType.PhonologicalRuleSynthesis: - return false; - } - - return false; - } - private static string GetRuleLabelString(IHCRule rule) { if (rule is Stratum) diff --git a/src/SIL.Machine.Morphology.HermitCrab/Trace.cs b/src/SIL.Machine.Morphology.HermitCrab/Trace.cs index 10abc27ab..425893450 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/Trace.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/Trace.cs @@ -135,6 +135,38 @@ internal Trace(TraceType type, IHCRule source) SubruleIndex = -1; } + private string _inputText; + private string _outputText; + + internal void SetTexts(string inputText, string outputText) + { + _inputText = inputText; + _outputText = outputText; + } + + private string FormatText(Word word) + { + return word?.GetTraceText(IsAnalysis(_type)); + } + + internal static bool IsAnalysis(TraceType type) + { + switch (type) + { + case TraceType.WordAnalysis: + case TraceType.LexicalLookup: + case TraceType.StratumAnalysisInput: + case TraceType.StratumAnalysisOutput: + case TraceType.TemplateAnalysisInput: + case TraceType.TemplateAnalysisOutput: + case TraceType.MorphologicalRuleAnalysis: + case TraceType.PhonologicalRuleAnalysis: + return true; + default: + return false; + } + } + public TraceType Type { get { return _type; } @@ -147,10 +179,33 @@ public IHCRule Source public int SubruleIndex { get; internal set; } + /// + /// Gets the word this step started from. It is null when the node has no input, and always + /// null when is false, because a retained word keeps + /// its whole shape alive for as long as the trace lives. Use in both modes. + /// public Word Input { get; internal set; } + /// + /// Gets the word this step produced. It is null when the node has no output, and always + /// null when is false. Use + /// in both modes. + /// public Word Output { get; internal set; } + /// + /// Gets the shape text of the input in its display form: the regular-expression form for analysis node + /// types and the plain form otherwise. It is null when the node has no input. With retained words + /// it is computed from on each read. + /// + public string InputText => _inputText ?? FormatText(Input); + + /// + /// Gets the shape text of the output, in the same form as . It is null when + /// the node has no output. + /// + public string OutputText => _outputText ?? FormatText(Output); + public FailureReason FailureReason { get; internal set; } } } diff --git a/src/SIL.Machine.Morphology.HermitCrab/TraceManager.cs b/src/SIL.Machine.Morphology.HermitCrab/TraceManager.cs index 3fa99c9b9..de1536f63 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/TraceManager.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/TraceManager.cs @@ -4,79 +4,108 @@ public class TraceManager : ITraceManager { public bool IsTracing { get; set; } + /// + /// Gets or sets whether trace nodes keep the words they describe. When true (the default), + /// and hold the words, and some nodes hold clones. + /// When false, both are always null and each node keeps only and + /// , so a trace no longer keeps every intermediate shape alive. The tree and + /// the text are the same in both modes. + /// + public bool RetainWords { get; set; } = true; + + private Trace Node( + TraceType type, + IHCRule source, + int subruleIndex = -1, + Word input = null, + Word output = null, + FailureReason reason = FailureReason.None, + bool cloneInput = false, + bool cloneOutput = false + ) + { + var trace = new Trace(type, source) { SubruleIndex = subruleIndex, FailureReason = reason }; + if (RetainWords) + { + trace.Input = cloneInput ? input?.Clone() : input; + trace.Output = cloneOutput ? output?.Clone() : output; + } + else + { + bool analysis = Trace.IsAnalysis(type); + trace.SetTexts(input?.GetTraceText(analysis), output?.GetTraceText(analysis)); + } + return trace; + } + public object GenerateWords(Language lang) { - return new Trace(TraceType.GenerateWords, lang); + return Node(TraceType.GenerateWords, lang); } public void AnalyzeWord(Language lang, Word input) { - input.CurrentTrace = new Trace(TraceType.WordAnalysis, lang) { Input = input }; + input.CurrentTrace = Node(TraceType.WordAnalysis, lang, input: input); } public void BeginUnapplyStratum(Stratum stratum, Word input) { - ((Trace)input.CurrentTrace).Children.Add( - new Trace(TraceType.StratumAnalysisInput, stratum) { Input = input } - ); + ((Trace)input.CurrentTrace).Children.Add(Node(TraceType.StratumAnalysisInput, stratum, input: input)); } public void EndUnapplyStratum(Stratum stratum, Word output) { - ((Trace)output.CurrentTrace).Children.Add( - new Trace(TraceType.StratumAnalysisOutput, stratum) { Output = output } - ); + ((Trace)output.CurrentTrace).Children.Add(Node(TraceType.StratumAnalysisOutput, stratum, output: output)); } public void PhonologicalRuleUnapplied(IPhonologicalRule rule, int subruleIndex, Word input, Word output) { ((Trace)output.CurrentTrace).Children.Add( - new Trace(TraceType.PhonologicalRuleAnalysis, rule) - { - SubruleIndex = subruleIndex, - Input = input, - Output = output.Clone(), - } + Node( + TraceType.PhonologicalRuleAnalysis, + rule, + subruleIndex: subruleIndex, + input: input, + output: output, + cloneOutput: true + ) ); } public void PhonologicalRuleNotUnapplied(IPhonologicalRule rule, int subruleIndex, Word input) { ((Trace)input.CurrentTrace).Children.Add( - new Trace(TraceType.PhonologicalRuleAnalysis, rule) - { - SubruleIndex = subruleIndex, - Input = input.Clone(), - } + Node( + TraceType.PhonologicalRuleAnalysis, + rule, + subruleIndex: subruleIndex, + input: input, + cloneInput: true + ) ); } public void BeginUnapplyTemplate(AffixTemplate template, Word input) { - ((Trace)input.CurrentTrace).Children.Add( - new Trace(TraceType.TemplateAnalysisInput, template) { Input = input } - ); + ((Trace)input.CurrentTrace).Children.Add(Node(TraceType.TemplateAnalysisInput, template, input: input)); } public void EndUnapplyTemplate(AffixTemplate template, Word output, bool unapplied, FailureReason reason) { ((Trace)output.CurrentTrace).Children.Add( - new Trace(TraceType.TemplateAnalysisOutput, template) - { - Output = unapplied ? output : null, - FailureReason = reason, - } + Node(TraceType.TemplateAnalysisOutput, template, output: unapplied ? output : null, reason: reason) ); } public void MorphologicalRuleUnapplied(IMorphologicalRule rule, int subruleIndex, Word input, Word output) { - var trace = new Trace(TraceType.MorphologicalRuleAnalysis, rule) - { - SubruleIndex = subruleIndex, - Input = input, - Output = output, - }; + var trace = Node( + TraceType.MorphologicalRuleAnalysis, + rule, + subruleIndex: subruleIndex, + input: input, + output: output + ); ((Trace)output.CurrentTrace).Children.Add(trace); output.CurrentTrace = trace; } @@ -84,7 +113,7 @@ public void MorphologicalRuleUnapplied(IMorphologicalRule rule, int subruleIndex public void MorphologicalRuleNotUnapplied(IMorphologicalRule rule, int subruleIndex, Word input) { ((Trace)input.CurrentTrace).Children.Add( - new Trace(TraceType.MorphologicalRuleAnalysis, rule) { SubruleIndex = subruleIndex, Input = input } + Node(TraceType.MorphologicalRuleAnalysis, rule, subruleIndex: subruleIndex, input: input) ); } @@ -97,12 +126,7 @@ object failureObj ) { ((Trace)input.CurrentTrace).Children.Add( - new Trace(TraceType.CompoundingRuleAnalysis, rule) - { - SubruleIndex = subruleIndex, - Input = input, - FailureReason = reason, - } + Node(TraceType.CompoundingRuleAnalysis, rule, subruleIndex: subruleIndex, input: input, reason: reason) ); } @@ -115,25 +139,20 @@ object failureObj ) { ((Trace)input.CurrentTrace).Children.Add( - new Trace(TraceType.CompoundingRuleSynthesis, rule) - { - SubruleIndex = subruleIndex, - Input = input, - FailureReason = reason, - } + Node(TraceType.CompoundingRuleSynthesis, rule, subruleIndex: subruleIndex, input: input, reason: reason) ); } public void LexicalLookup(Stratum stratum, Word input) { ((Trace)input.CurrentTrace).Children.Add( - new Trace(TraceType.LexicalLookup, stratum) { Input = input.Clone() } + Node(TraceType.LexicalLookup, stratum, input: input, cloneInput: true) ); } public void SynthesizeWord(Language lang, Word input) { - var trace = new Trace(TraceType.WordSynthesis, lang) { Input = input.Clone() }; + var trace = Node(TraceType.WordSynthesis, lang, input: input, cloneInput: true); var curTrace = (Trace)input.CurrentTrace; curTrace.Children.Last.Children.Add(trace); input.CurrentTrace = trace; @@ -141,49 +160,39 @@ public void SynthesizeWord(Language lang, Word input) public void BeginApplyStratum(Stratum stratum, Word input) { - ((Trace)input.CurrentTrace).Children.Add( - new Trace(TraceType.StratumSynthesisInput, stratum) { Input = input } - ); + ((Trace)input.CurrentTrace).Children.Add(Node(TraceType.StratumSynthesisInput, stratum, input: input)); } public void NonFinalTemplateAppliedLast(Stratum stratum, Word word) { ((Trace)word.CurrentTrace).Children.Add( - new Trace(TraceType.StratumSynthesisOutput, stratum) - { - Output = word, - FailureReason = FailureReason.PartialParse, - } + Node(TraceType.StratumSynthesisOutput, stratum, output: word, reason: FailureReason.PartialParse) ); } public void ApplicableTemplatesNotApplied(Stratum stratum, Word word) { ((Trace)word.CurrentTrace).Children.Add( - new Trace(TraceType.StratumSynthesisOutput, stratum) - { - Output = word, - FailureReason = FailureReason.PartialParse, - } + Node(TraceType.StratumSynthesisOutput, stratum, output: word, reason: FailureReason.PartialParse) ); } public void EndApplyStratum(Stratum stratum, Word output) { - ((Trace)output.CurrentTrace).Children.Add( - new Trace(TraceType.StratumSynthesisOutput, stratum) { Output = output } - ); + ((Trace)output.CurrentTrace).Children.Add(Node(TraceType.StratumSynthesisOutput, stratum, output: output)); } public void PhonologicalRuleApplied(IPhonologicalRule rule, int subruleIndex, Word input, Word output) { ((Trace)output.CurrentTrace).Children.Add( - new Trace(TraceType.PhonologicalRuleSynthesis, rule) - { - SubruleIndex = subruleIndex, - Input = input, - Output = output.Clone(), - } + Node( + TraceType.PhonologicalRuleSynthesis, + rule, + subruleIndex: subruleIndex, + input: input, + output: output, + cloneOutput: true + ) ); } @@ -196,37 +205,38 @@ object failureObj ) { ((Trace)input.CurrentTrace).Children.Add( - new Trace(TraceType.PhonologicalRuleSynthesis, rule) - { - SubruleIndex = subruleIndex, - Input = input.Clone(), - FailureReason = reason, - } + Node( + TraceType.PhonologicalRuleSynthesis, + rule, + subruleIndex: subruleIndex, + input: input, + cloneInput: true, + reason: reason + ) ); } public void BeginApplyTemplate(AffixTemplate template, Word input) { - ((Trace)input.CurrentTrace).Children.Add( - new Trace(TraceType.TemplateSynthesisInput, template) { Input = input } - ); + ((Trace)input.CurrentTrace).Children.Add(Node(TraceType.TemplateSynthesisInput, template, input: input)); } public void EndApplyTemplate(AffixTemplate template, Word output, bool applied) { ((Trace)output.CurrentTrace).Children.Add( - new Trace(TraceType.TemplateSynthesisOutput, template) { Output = applied ? output : null } + Node(TraceType.TemplateSynthesisOutput, template, output: applied ? output : null) ); } public void MorphologicalRuleApplied(IMorphologicalRule rule, int subruleIndex, Word input, Word output) { - var trace = new Trace(TraceType.MorphologicalRuleSynthesis, rule) - { - SubruleIndex = subruleIndex, - Input = input, - Output = output, - }; + var trace = Node( + TraceType.MorphologicalRuleSynthesis, + rule, + subruleIndex: subruleIndex, + input: input, + output: output + ); ((Trace)output.CurrentTrace).Children.Add(trace); output.CurrentTrace = trace; } @@ -240,30 +250,29 @@ object failureObj ) { ((Trace)input.CurrentTrace).Children.Add( - new Trace(TraceType.MorphologicalRuleSynthesis, rule) - { - SubruleIndex = subruleIndex, - Input = input, - FailureReason = reason, - } + Node( + TraceType.MorphologicalRuleSynthesis, + rule, + subruleIndex: subruleIndex, + input: input, + reason: reason + ) ); } public void Blocked(IHCRule rule, Word output) { - ((Trace)output.CurrentTrace).Children.Add(new Trace(TraceType.Blocked, rule) { Output = output }); + ((Trace)output.CurrentTrace).Children.Add(Node(TraceType.Blocked, rule, output: output)); } public void Successful(Language lang, Word word) { - ((Trace)word.CurrentTrace).Children.Add(new Trace(TraceType.Successful, lang) { Output = word }); + ((Trace)word.CurrentTrace).Children.Add(Node(TraceType.Successful, lang, output: 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 } - ); + ((Trace)word.CurrentTrace).Children.Add(Node(TraceType.Failed, lang, output: word, reason: reason)); } } } diff --git a/src/SIL.Machine.Morphology.HermitCrab/Word.cs b/src/SIL.Machine.Morphology.HermitCrab/Word.cs index a1722a18a..514dd04e5 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/Word.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/Word.cs @@ -220,6 +220,32 @@ public IEnumerable MorphemesInApplicationOrder public object CurrentTrace { get; set; } + // Display text for traces, kept only on frozen words and never copied by the copy constructor. + private string _traceTextRegex; + private string _traceTextPlain; + + internal string GetTraceText(bool analysis) + { + // Unsynchronised on purpose: a racing writer stores an equal string. + if (!IsFrozen) + return FormatTraceText(analysis); + if (analysis) + { + if (_traceTextRegex == null) + _traceTextRegex = FormatTraceText(true); + return _traceTextRegex; + } + if (_traceTextPlain == null) + _traceTextPlain = FormatTraceText(false); + return _traceTextPlain; + } + + private string FormatTraceText(bool analysis) + { + CharacterDefinitionTable table = Stratum.CharacterDefinitionTable; + return analysis ? Shape.ToRegexString(table, true) : Shape.ToString(table, true); + } + public bool IsPartial { get { return _isPartial; } diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/LeanTraceTests.cs b/tests/SIL.Machine.Morphology.HermitCrab.Tests/LeanTraceTests.cs new file mode 100644 index 000000000..2105141a6 --- /dev/null +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/LeanTraceTests.cs @@ -0,0 +1,375 @@ +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; + +namespace SIL.Machine.Morphology.HermitCrab; + +public class LeanTraceTests : HermitCrabTestBase +{ + private static readonly string[] Words = + { + "sagd", + "sagdv", + "sagdvs", + "sagzv", + "sid", + "sau", + "pʰitʰ", + "datʰ", + "gab", + "sagdvsagd", + "datpʰut", + "pʰutdat", + "sagpʰut", + }; + + [Test] + public void LeanTrace_MatchesFullTraceNodeForNode() + { + BuildScenario(); + var seenTypes = new HashSet(); + var seenReasons = new HashSet(); + int nodes = 0; + foreach (string word in Words) + { + Trace full = ParseWithTrace(word, retainWords: true); + Trace lean = ParseWithTrace(word, retainWords: false); + nodes += AssertSameTree(full, lean, seenTypes, seenReasons); + } + + Assert.That(nodes, Is.GreaterThan(100)); + TraceType[] expectedTypes = + { + TraceType.WordAnalysis, + TraceType.StratumAnalysisInput, + TraceType.StratumAnalysisOutput, + TraceType.PhonologicalRuleAnalysis, + TraceType.PhonologicalRuleSynthesis, + TraceType.TemplateAnalysisInput, + TraceType.TemplateAnalysisOutput, + TraceType.TemplateSynthesisInput, + TraceType.TemplateSynthesisOutput, + TraceType.MorphologicalRuleAnalysis, + TraceType.MorphologicalRuleSynthesis, + TraceType.CompoundingRuleAnalysis, + TraceType.LexicalLookup, + TraceType.WordSynthesis, + TraceType.StratumSynthesisInput, + TraceType.StratumSynthesisOutput, + TraceType.Successful, + TraceType.Failed, + TraceType.Blocked, + }; + foreach (TraceType type in expectedTypes) + Assert.That(seenTypes, Does.Contain(type), type.ToString()); + Assert.That(seenReasons.Count, Is.GreaterThan(1)); + } + + [Test] + public void LeanTrace_KeepsNoWords() + { + BuildScenario(); + foreach (string word in Words) + { + Trace lean = ParseWithTrace(word, retainWords: false); + AssertNoWords(lean); + } + } + + [Test] + public void FullTrace_StillKeepsWords() + { + BuildScenario(); + Trace full = ParseWithTrace("sagd", retainWords: true); + Assert.That(full.Input, Is.Not.Null); + Assert.That(Flatten(full).Any(t => t.Output != null), Is.True); + } + + [Test] + public void ParseWord_IsIdenticalWithTracingOffFullAndLean() + { + BuildScenario(); + foreach (string word in Words) + { + var off = new Morpher(new TraceManager { IsTracing = false }, Language, 1); + string expected = Describe(off.ParseWord(word)); + Assert.That(Describe(ParseWords(word, retainWords: true)), Is.EqualTo(expected), word); + Assert.That(Describe(ParseWords(word, retainWords: false)), Is.EqualTo(expected), word); + } + } + + [Test] + public void FrozenWord_CachesItsTraceTextAndCopiesDoNotInherit() + { + BuildScenario(); + var morpher = new Morpher(new TraceManager(), Language, 1); + Word word = morpher.ParseWord("sagd").First(); + Assert.That(word.IsFrozen, Is.True); + string first = word.GetTraceText(false); + Assert.That(word.GetTraceText(false), Is.SameAs(first)); + Assert.That(word.GetTraceText(true), Is.Not.Null); + + Word copy = word.Clone(); + Assert.That(copy.IsFrozen, Is.False); + Assert.That(copy.GetTraceText(false), Is.EqualTo(first)); + Assert.That(copy.GetTraceText(false), Is.Not.SameAs(first)); + } + + private Trace ParseWithTrace(string word, bool retainWords) + { + var manager = new TraceManager { IsTracing = true, RetainWords = retainWords }; + var morpher = new Morpher(manager, Language, 1); + morpher.ParseWord(word, out object trace); + return (Trace)trace; + } + + private IEnumerable ParseWords(string word, bool retainWords) + { + var manager = new TraceManager { IsTracing = true, RetainWords = retainWords }; + return new Morpher(manager, Language, 1).ParseWord(word).ToArray(); + } + + private static string Describe(IEnumerable words) + { + return string.Join( + "|", + words + .Select(w => + w.Shape.ToString(w.Stratum.CharacterDefinitionTable, true) + + ":" + + string.Join(" ", w.AllomorphsInMorphOrder.Select(a => a.Morpheme.Gloss)) + ) + .OrderBy(s => s, StringComparer.Ordinal) + ); + } + + private static IEnumerable Flatten(Trace trace) + { + yield return trace; + foreach (Trace child in trace.Children) + { + foreach (Trace descendant in Flatten(child)) + yield return descendant; + } + } + + private static void AssertNoWords(Trace trace) + { + foreach (Trace node in Flatten(trace)) + { + Assert.That(node.Input, Is.Null); + Assert.That(node.Output, Is.Null); + } + } + + private static int AssertSameTree( + Trace full, + Trace lean, + HashSet seenTypes, + HashSet seenReasons + ) + { + Assert.That(lean.Input, Is.Null); + Assert.That(lean.Output, Is.Null); + Assert.That(lean.Type, Is.EqualTo(full.Type)); + Assert.That(lean.Source, Is.SameAs(full.Source)); + Assert.That(lean.SubruleIndex, Is.EqualTo(full.SubruleIndex)); + Assert.That(lean.FailureReason, Is.EqualTo(full.FailureReason)); + Assert.That(lean.InputText, Is.EqualTo(full.InputText)); + Assert.That(lean.OutputText, Is.EqualTo(full.OutputText)); + Assert.That(full.InputText, Is.EqualTo(PreviousCliText(full.Type, full.Input))); + Assert.That(full.OutputText, Is.EqualTo(PreviousCliText(full.Type, full.Output))); + Assert.That(lean.Children.Count, Is.EqualTo(full.Children.Count)); + seenTypes.Add(full.Type); + seenReasons.Add(full.FailureReason); + + int count = 1; + Trace[] fullChildren = full.Children.ToArray(); + Trace[] leanChildren = lean.Children.ToArray(); + for (int i = 0; i < fullChildren.Length; i++) + count += AssertSameTree(fullChildren[i], leanChildren[i], seenTypes, seenReasons); + return count; + } + + // The rule the CLI used before it read InputText and OutputText. + private static string? PreviousCliText(TraceType type, Word word) + { + if (word == null) + return null; + bool analysis = + type + is TraceType.WordAnalysis + or TraceType.LexicalLookup + or TraceType.StratumAnalysisInput + or TraceType.StratumAnalysisOutput + or TraceType.TemplateAnalysisInput + or TraceType.TemplateAnalysisOutput + or TraceType.MorphologicalRuleAnalysis + or TraceType.PhonologicalRuleAnalysis; + CharacterDefinitionTable table = word.Stratum.CharacterDefinitionTable; + return analysis ? word.Shape.ToRegexString(table, true) : word.Shape.ToString(table, true); + } + + private void BuildScenario() + { + var any = FeatureStruct.New().Symbol(HCFeatureSystem.Segment).Value; + var asp = FeatureStruct + .New(Language.PhonologicalFeatureSystem) + .Symbol(HCFeatureSystem.Segment) + .Symbol("asp+") + .Value; + var nonCons = FeatureStruct + .New(Language.PhonologicalFeatureSystem) + .Symbol(HCFeatureSystem.Segment) + .Symbol("cons-") + .Value; + + var rule1 = new RewriteRule + { + Name = "rule1", + Lhs = Pattern.New().Annotation(Character(Table1, "t")).Value, + }; + Allophonic.PhonologicalRules.Add(rule1); + rule1.Subrules.Add( + new RewriteSubrule + { + Rhs = Pattern.New().Annotation(asp).Value, + LeftEnvironment = Pattern.New().Annotation(nonCons).Value, + } + ); + var rule2 = new RewriteRule + { + Name = "rule2", + Lhs = Pattern.New().Annotation(Character(Table3, "p")).Value, + }; + Allophonic.PhonologicalRules.Add(rule2); + Morphophonemic.PhonologicalRules.Add(rule2); + rule2.Subrules.Add( + new RewriteSubrule + { + Rhs = Pattern.New().Annotation(asp).Value, + RightEnvironment = Pattern.New().Annotation(nonCons).Value, + } + ); + + var edSuffix = new RealizationalAffixProcessRule + { + Name = "ed_suffix", + RealizationalFeatureStruct = FeatureStruct + .New(Language.SyntacticFeatureSystem) + .Feature(Head) + .EqualTo(head => head.Feature("tense").EqualTo("past")) + .Value, + Gloss = "PAST", + }; + edSuffix.Allomorphs.Add( + new AffixProcessAllomorph + { + Lhs = { Pattern.New("1").Annotation(any).OneOrMore.Value }, + Rhs = { new CopyFromInput("1"), new InsertSegments(Table3, "d") }, + } + ); + var sSuffix = new RealizationalAffixProcessRule + { + Name = "s_suffix", + RealizationalFeatureStruct = FeatureStruct + .New(Language.SyntacticFeatureSystem) + .Feature(Head) + .EqualTo(head => head.Feature("pers").EqualTo("3").Feature("tense").EqualTo("pres")) + .Value, + Gloss = "3SG", + }; + sSuffix.Allomorphs.Add( + new AffixProcessAllomorph + { + Lhs = { Pattern.New("1").Annotation(any).OneOrMore.Value }, + Rhs = { new CopyFromInput("1"), new InsertSegments(Table3, "z") }, + } + ); + var evidential = new RealizationalAffixProcessRule + { + Name = "evidential", + RealizationalFeatureStruct = FeatureStruct + .New(Language.SyntacticFeatureSystem) + .Feature(Head) + .EqualTo(head => head.Feature("evidential").EqualTo("witnessed")) + .Value, + Gloss = "WIT", + }; + evidential.Allomorphs.Add( + new AffixProcessAllomorph + { + Lhs = { Pattern.New("1").Annotation(any).OneOrMore.Value }, + Rhs = { new CopyFromInput("1"), new InsertSegments(Table3, "v") }, + } + ); + var verbTemplate = new AffixTemplate + { + Name = "verb", + RequiredSyntacticFeatureStruct = FeatureStruct.New(Language.SyntacticFeatureSystem).Symbol("V").Value, + }; + verbTemplate.Slots.Add(new AffixTemplateSlot(sSuffix, edSuffix) { Optional = true }); + verbTemplate.Slots.Add(new AffixTemplateSlot(evidential) { Optional = true }); + Morphophonemic.AffixTemplates.Add(verbTemplate); + + var nominalizer = new AffixProcessRule + { + Name = "nominalizer", + Gloss = "NOM", + RequiredSyntacticFeatureStruct = FeatureStruct.New(Language.SyntacticFeatureSystem).Symbol("V").Value, + OutSyntacticFeatureStruct = FeatureStruct.New(Language.SyntacticFeatureSystem).Symbol("N").Value, + }; + nominalizer.Allomorphs.Add( + new AffixProcessAllomorph + { + Lhs = { Pattern.New("1").Annotation(any).OneOrMore.Value }, + Rhs = { new CopyFromInput("1"), new InsertSegments(Table3, "s") }, + } + ); + Morphophonemic.MorphologicalRules.Add(nominalizer); + + var crule = new CompoundingRule + { + Name = "compound", + HeadRequiredSyntacticFeatureStruct = FeatureStruct.New(Language.SyntacticFeatureSystem).Symbol("V").Value, + NonHeadRequiredSyntacticFeatureStruct = FeatureStruct + .New(Language.SyntacticFeatureSystem) + .Symbol("N") + .Value, + OutSyntacticFeatureStruct = FeatureStruct.New(Language.SyntacticFeatureSystem).Symbol("N").Value, + }; + crule.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(Table3, "+"), new CopyFromInput("nonHead") }, + } + ); + Allophonic.MorphologicalRules.Add(crule); + + var restricted = new CompoundingRule + { + Name = "restricted_compound", + HeadRequiredSyntacticFeatureStruct = FeatureStruct.New(Language.SyntacticFeatureSystem).Symbol("V").Value, + NonHeadRequiredSyntacticFeatureStruct = FeatureStruct + .New(Language.SyntacticFeatureSystem) + .Symbol("N") + .Value, + OutSyntacticFeatureStruct = FeatureStruct.New(Language.SyntacticFeatureSystem).Symbol("N").Value, + }; + restricted.NonHeadProdRestrictionsMprFeatures.Add(Latinate); + restricted.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(Table3, "+"), new CopyFromInput("nonHead") }, + } + ); + Allophonic.MorphologicalRules.Add(restricted); + } +}