diff --git a/src/SIL.Machine.Morphology.HermitCrab/PhonologicalRules/NarrowAnalysisRewriteRuleSpec.cs b/src/SIL.Machine.Morphology.HermitCrab/PhonologicalRules/NarrowAnalysisRewriteRuleSpec.cs index 6380eaab1..19873e9ec 100644 --- a/src/SIL.Machine.Morphology.HermitCrab/PhonologicalRules/NarrowAnalysisRewriteRuleSpec.cs +++ b/src/SIL.Machine.Morphology.HermitCrab/PhonologicalRules/NarrowAnalysisRewriteRuleSpec.cs @@ -30,17 +30,32 @@ RewriteSubrule subrule } else { - Pattern.Children.AddRange(subrule.Rhs.Children.CloneItems()); + // The matcher skips optional nodes and boundaries, so the matched RHS nodes need not be + // adjacent; each is recovered from its own group capture, which a quantifier may leave empty. + int i = 0; + foreach (PatternNode node in subrule.Rhs.Children) + { + Pattern.Children.Add(new Group("target" + i) { Children = { node.Clone() } }); + i++; + } } Pattern.Freeze(); SubruleSpecs.Add(new AnalysisRewriteSubruleSpec(matcherSettings, subrule, Unapply)); } + private ShapeNode GetTargetNode(Match targetMatch, int i) + { + if (i >= _targetCount) + return null; + GroupCapture capture = targetMatch.GroupCaptures["target" + i]; + return capture.Success ? capture.Range.GetStart(targetMatch.Matcher.Direction) : null; + } + private void Unapply(Match targetMatch, Range range, VariableBindings varBindings) { ShapeNode curNode = IsTargetEmpty ? range.Start : range.End; - ShapeNode targetNode = range.Start; + ShapeNode targetNode = GetTargetNode(targetMatch, 0); int targetPos = 0; bool zipper = true; foreach ( @@ -54,7 +69,7 @@ Constraint constraint in _analysisRhs.Children.CastToConstraint fs.ReplaceVariables(varBindings); if ( zipper - && targetPos < _targetCount + && targetNode != null && constraint.Type() == HCFeatureSystem.Segment && targetNode.Type() == HCFeatureSystem.Segment && !fs.HasVariables // This can be removed when Union is fixed. @@ -63,8 +78,8 @@ Constraint constraint in _analysisRhs.Children.CastToConstraint { // Union fs into the existing node's feature structure. targetNode.Annotation.FeatureStruct.Union(fs); - targetNode = targetNode.Next; targetPos++; + targetNode = GetTargetNode(targetMatch, targetPos); } else { @@ -77,8 +92,9 @@ Constraint constraint in _analysisRhs.Children.CastToConstraint // Make the rest of the existing nodes optional. for (int i = targetPos; i < _targetCount; i++) { - targetNode.Annotation.Optional = true; - targetNode = targetNode.Next; + ShapeNode node = GetTargetNode(targetMatch, i); + if (node != null) + node.Annotation.Optional = true; } } } diff --git a/tests/SIL.Machine.Morphology.HermitCrab.Tests/PhonologicalRules/RewriteRuleTests.cs b/tests/SIL.Machine.Morphology.HermitCrab.Tests/PhonologicalRules/RewriteRuleTests.cs index be7135bc3..b8fcbf71f 100644 --- a/tests/SIL.Machine.Morphology.HermitCrab.Tests/PhonologicalRules/RewriteRuleTests.cs +++ b/tests/SIL.Machine.Morphology.HermitCrab.Tests/PhonologicalRules/RewriteRuleTests.cs @@ -646,6 +646,226 @@ public void ExpandRules() AssertMorphsEqual(morpher.ParseWord("biiiibiiii"), "27"); } + [Test] + public void ExpandRuleUnappliesAcrossInterposedOptionalSegment() + { + var highVowel = FeatureStruct + .New(Language.PhonologicalFeatureSystem) + .Symbol(HCFeatureSystem.Segment) + .Symbol("cons-") + .Symbol("voc+") + .Symbol("high+") + .Value; + var backRndVowel = FeatureStruct + .New(Language.PhonologicalFeatureSystem) + .Symbol(HCFeatureSystem.Segment) + .Symbol("cons-") + .Symbol("voc+") + .Symbol("back+") + .Symbol("round+") + .Value; + + var expand = new RewriteRule + { + Name = "expand", + Lhs = Pattern.New().Annotation(backRndVowel).Value, + }; + Allophonic.PhonologicalRules.Add(expand); + expand.Subrules.Add( + new RewriteSubrule + { + Rhs = Pattern.New().Annotation(highVowel).Annotation(highVowel).Value, + } + ); + + // Ordered after the expansion, so it unapplies first and leaves an optional z between the two i's. + var deletion = new RewriteRule + { + Name = "deletion", + Lhs = Pattern.New().Annotation(Character(Table1, "z")).Value, + }; + Allophonic.PhonologicalRules.Add(deletion); + deletion.Subrules.Add( + new RewriteSubrule + { + LeftEnvironment = Pattern.New().Annotation(Character(Table1, "i")).Value, + RightEnvironment = Pattern.New().Annotation(Character(Table1, "i")).Value, + } + ); + + var morpher = new Morpher(TraceManager, Language); + AssertMorphsEqual(morpher.ParseWord("biiiibiiii"), "27"); + } + + [Test] + public void MergeRuleUnappliesAcrossInterposedOptionalSegment() + { + var merge = new RewriteRule + { + Name = "merge", + Lhs = Pattern + .New() + .Annotation(Character(Table3, "s")) + .Annotation(Character(Table3, "a")) + .Annotation(Character(Table3, "g")) + .Value, + }; + Morphophonemic.PhonologicalRules.Add(merge); + merge.Subrules.Add( + new RewriteSubrule + { + Rhs = Pattern + .New() + .Annotation(Character(Table3, "n")) + .Annotation(Character(Table3, "i")) + .Value, + } + ); + + var morpher = new Morpher(TraceManager, Language); + AssertMorphsEqual(morpher.ParseWord("ni"), "32"); + + // Ordered after the merge, so it unapplies first and leaves an optional z inside the span n..i. + var deletion = new RewriteRule + { + Name = "deletion", + Lhs = Pattern.New().Annotation(Character(Table3, "z")).Value, + }; + Morphophonemic.PhonologicalRules.Add(deletion); + deletion.Subrules.Add( + new RewriteSubrule + { + LeftEnvironment = Pattern.New().Annotation(Character(Table3, "n")).Value, + RightEnvironment = Pattern.New().Annotation(Character(Table3, "i")).Value, + } + ); + + morpher = new Morpher(TraceManager, Language); + AssertMorphsEqual(morpher.ParseWord("ni"), "32"); + } + + [Test] + public void RightToLeftMergeRuleUnappliesAcrossInterposedOptionalSegment() + { + var merge = new RewriteRule + { + Name = "merge", + Direction = Direction.RightToLeft, + Lhs = Pattern + .New() + .Annotation(Character(Table3, "s")) + .Annotation(Character(Table3, "a")) + .Annotation(Character(Table3, "g")) + .Value, + }; + Morphophonemic.PhonologicalRules.Add(merge); + merge.Subrules.Add( + new RewriteSubrule + { + Rhs = Pattern + .New() + .Annotation(Character(Table3, "n")) + .Annotation(Character(Table3, "i")) + .Value, + } + ); + + var morpher = new Morpher(TraceManager, Language); + AssertMorphsEqual(morpher.ParseWord("ni"), "32"); + + // Ordered after the merge, so it unapplies first and leaves an optional z inside the span n..i. + var deletion = new RewriteRule + { + Name = "deletion", + Lhs = Pattern.New().Annotation(Character(Table3, "z")).Value, + }; + Morphophonemic.PhonologicalRules.Add(deletion); + deletion.Subrules.Add( + new RewriteSubrule + { + LeftEnvironment = Pattern.New().Annotation(Character(Table3, "n")).Value, + RightEnvironment = Pattern.New().Annotation(Character(Table3, "i")).Value, + } + ); + + morpher = new Morpher(TraceManager, Language); + AssertMorphsEqual(morpher.ParseWord("ni"), "32"); + } + + [Test] + public void MergeRuleUnappliesAcrossInterposedOptionalBoundary() + { + var merge = new RewriteRule + { + Name = "merge", + Lhs = Pattern + .New() + .Annotation(Character(Table3, "s")) + .Annotation(Character(Table3, "a")) + .Annotation(Character(Table3, "g")) + .Value, + }; + Morphophonemic.PhonologicalRules.Add(merge); + merge.Subrules.Add( + new RewriteSubrule + { + Rhs = Pattern + .New() + .Annotation(Character(Table3, "n")) + .Annotation(Character(Table3, "i")) + .Value, + } + ); + + // Ordered after the merge, so it unapplies first and leaves an optional boundary inside the span n..i. + var deletion = new RewriteRule + { + Name = "deletion", + Lhs = Pattern.New().Annotation(Character(Table3, "+")).Value, + }; + Morphophonemic.PhonologicalRules.Add(deletion); + deletion.Subrules.Add( + new RewriteSubrule + { + LeftEnvironment = Pattern.New().Annotation(Character(Table3, "n")).Value, + RightEnvironment = Pattern.New().Annotation(Character(Table3, "i")).Value, + } + ); + + var morpher = new Morpher(TraceManager, Language); + AssertMorphsEqual(morpher.ParseWord("ni"), "32"); + } + + [Test] + public void MergeRuleWithOptionalReplacementSegmentSkipsEmptyCapture() + { + var merge = new RewriteRule + { + Name = "merge", + Lhs = Pattern + .New() + .Annotation(Character(Table3, "s")) + .Annotation(Character(Table3, "a")) + .Annotation(Character(Table3, "g")) + .Value, + }; + Morphophonemic.PhonologicalRules.Add(merge); + merge.Subrules.Add( + new RewriteSubrule + { + Rhs = Pattern + .New() + .Annotation(Character(Table3, "p")) + .Annotation(Character(Table3, "i")) + .Optional.Value, + } + ); + + // The optional i captures nothing in "pu"; this checks that the empty capture is skipped rather than throwing. + var morpher = new Morpher(TraceManager, Language); + AssertMorphsEqual(morpher.ParseWord("pu"), "52"); + } + [Test] public void BoundaryRules() {