From 9f7828e85755316dba6eb5a708c069feba5259ea Mon Sep 17 00:00:00 2001 From: John Lambert Date: Tue, 27 Oct 2026 15:48:14 -0400 Subject: [PATCH] Add TraceManager.RetainWords to keep trace text, not Words With RetainWords false, trace nodes store the shape text of their input and output (InputText, OutputText) instead of Word references and clones. The default is unchanged and the CLI turns lean mode on by setting it false. Co-Authored-By: Claude Opus 5.5 --- .../HCContext.cs | 2 +- .../ParseCommand.cs | 47 +-- .../Trace.cs | 55 +++ .../TraceManager.cs | 203 +++++----- src/SIL.Machine.Morphology.HermitCrab/Word.cs | 26 ++ .../LeanTraceTests.cs | 375 ++++++++++++++++++ 6 files changed, 567 insertions(+), 141 deletions(-) create mode 100644 tests/SIL.Machine.Morphology.HermitCrab.Tests/LeanTraceTests.cs 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); + } +}