Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ private void AddGroup(Dictionary<string, Group<Word, ShapeNode>> groups, string
{
var newGroup = new Group<Word, ShapeNode>(name);
foreach (
Constraint<Word, ShapeNode> constraint in groups[name].Children.Cast<Constraint<Word, ShapeNode>>()
Constraint<Word, ShapeNode> constraint in groups[name]
.Children.CastToConstraints("A switch group of a metathesis rule")
)
{
Constraint<Word, ShapeNode> newConstraint = constraint.Clone();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Linq;
using SIL.Machine.Annotations;
using SIL.Machine.FeatureModel;
using SIL.Machine.Matching;
Expand All @@ -15,7 +14,11 @@ public EpenthesisAnalysisRewriteRuleSpec(MatcherSettings<ShapeNode> matcherSetti
Pattern.Acceptable = IsUnapplicationNonvacuous;
_targetCount = subrule.Rhs.Children.Count;

foreach (Constraint<Word, ShapeNode> constraint in subrule.Rhs.Children.Cast<Constraint<Word, ShapeNode>>())
foreach (
Constraint<Word, ShapeNode> constraint in subrule.Rhs.Children.CastToConstraints(
"The replacement (right-hand side) of an epenthesis rewrite subrule"
)
)
{
Constraint<Word, ShapeNode> newConstraint = constraint.Clone();
newConstraint.FeatureStruct.AddValue(HCFeatureSystem.Modified, HCFeatureSystem.Clean);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,12 @@ Tuple<PatternNode<Word, ShapeNode>, PatternNode<Word, ShapeNode>> tuple in lhs.C
)
)
{
var lhsConstraint = (Constraint<Word, ShapeNode>)tuple.Item1;
var rhsConstraint = (Constraint<Word, ShapeNode>)tuple.Item2;
Constraint<Word, ShapeNode> lhsConstraint = tuple.Item1.AsConstraint(
"The target (left-hand side) of a rewrite rule"
);
Constraint<Word, ShapeNode> rhsConstraint = tuple.Item2.AsConstraint(
"The replacement (right-hand side) of a rewrite subrule"
);

if (lhsConstraint.Type() == HCFeatureSystem.Segment && rhsConstraint.Type() == HCFeatureSystem.Segment)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Linq;
using SIL.Extensions;
using SIL.Machine.Annotations;
using SIL.Machine.FeatureModel;
Expand Down Expand Up @@ -42,7 +41,9 @@ private void Unapply(Match<Word, ShapeNode> targetMatch, Range<ShapeNode> range,
{
ShapeNode curNode = IsTargetEmpty ? range.Start : range.End;
foreach (
Constraint<Word, ShapeNode> constraint in _analysisRhs.Children.Cast<Constraint<Word, ShapeNode>>()
Constraint<Word, ShapeNode> constraint in _analysisRhs.Children.CastToConstraints(
"The target (left-hand side) of a rewrite rule"
)
)
{
FeatureStruct fs = constraint.FeatureStruct.Clone();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System.Collections.Generic;
using SIL.Machine.Annotations;
using SIL.Machine.Matching;

namespace SIL.Machine.Morphology.HermitCrab.PhonologicalRules
{
/// <summary>
/// Rewrite and metathesis rule specs assume that every top-level child of a rule's target/replacement
/// pattern (or a metathesis switch group) is a simple <see cref="Constraint{TData, TOffset}"/> (a
/// segment, natural class, or boundary marker). <see cref="Constraint{TData, TOffset}"/> and
/// <see cref="Quantifier{TData, TOffset}"/> are both direct subclasses of
/// <see cref="PatternNode{TData, TOffset}"/> -- siblings, not related by inheritance -- but the DTD and
/// <c>XmlLanguageLoader</c> allow an <c>OptionalSegmentSequence</c> (loaded as a <see cref="Quantifier{TData, TOffset}"/>)
/// in exactly the same positions as a plain segment. These helpers replace unchecked
/// <c>Cast&lt;Constraint&lt;Word, ShapeNode&gt;&gt;()</c> calls and direct casts with a checked
/// conversion that raises a clear, actionable <see cref="CompileException"/> naming the unsupported
/// construct and where it appeared, instead of an opaque <see cref="System.InvalidCastException"/>.
/// </summary>
internal static class PatternNodeCastExtensions
{
public static IEnumerable<Constraint<Word, ShapeNode>> CastToConstraints(
this IEnumerable<PatternNode<Word, ShapeNode>> nodes,
string context
)
{
foreach (PatternNode<Word, ShapeNode> node in nodes)
yield return node.AsConstraint(context);
}

public static Constraint<Word, ShapeNode> AsConstraint(this PatternNode<Word, ShapeNode> node, string context)
{
if (node is Constraint<Word, ShapeNode> constraint)
return constraint;

throw new CompileException(
$"{context} contains a '{node.GetType().Name}', which is not supported there. Only simple "
+ "segments, natural classes, and boundary markers are supported; optional or repeated "
+ "segment sequences (quantifiers) are only supported within a rule's left/right environment."
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ string rightGroupName
{
var newGroup = new Group<Word, ShapeNode>(group.Name);
foreach (
Constraint<Word, ShapeNode> constraint in group.Children.Cast<Constraint<Word, ShapeNode>>()
Constraint<Word, ShapeNode> constraint in group.Children.CastToConstraints(
"A switch group of a metathesis rule"
)
)
{
Constraint<Word, ShapeNode> newConstraint = constraint.Clone();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ IEnumerable<RewriteSubrule> subrules
}
else
{
foreach (Constraint<Word, ShapeNode> constraint in lhs.Children.Cast<Constraint<Word, ShapeNode>>())
foreach (
Constraint<Word, ShapeNode> constraint in lhs.Children.CastToConstraints(
"The target (left-hand side) of a rewrite rule"
)
)
{
var newConstraint = constraint.Clone();
if (isIterative)
Expand Down Expand Up @@ -90,7 +94,9 @@ Tuple<ShapeNode, PatternNode<Word, ShapeNode>> tuple in match
.Zip(lhs.Children)
)
{
var constraints = (Constraint<Word, ShapeNode>)tuple.Item2;
Constraint<Word, ShapeNode> constraints = tuple.Item2.AsConstraint(
"The target (left-hand side) of a rewrite rule"
);
if (tuple.Item1.Annotation.Type() != constraints.Type())
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1923,4 +1923,89 @@ public void MultipleApplicationRules()
morpher = new Morpher(TraceManager, Language);
AssertMorphsEqual(morpher.ParseWord("gigugi"), "44");
}

// Constraint<TData, TOffset> and Quantifier<TData, TOffset> are both direct subclasses of
// PatternNode<TData, TOffset> (siblings, not related by inheritance). The DTD permits an
// OptionalSegmentSequence (loaded as a Quantifier) anywhere a plain Segment/SimpleContext is
// permitted in a PhoneticSequence, including as the target (Lhs) or replacement (Rhs) of a rewrite
// rule -- not just in its environments. Before the fix, the rule specs assumed every top-level
// Lhs/Rhs child was a Constraint and cast accordingly, so a DTD-legal Quantifier there crashed with
// an unhandled InvalidCastException deep inside Morpher construction. These tests build such rules
// directly (bypassing the XML loader, like the rest of this file) and confirm that constructing a
// Morpher over them now fails fast with a clear, typed CompileException instead.
[Test]
public void QuantifierInTargetIsRejectedWithClearError()
{
var highVowel = FeatureStruct
.New(Language.PhonologicalFeatureSystem)
.Symbol(HCFeatureSystem.Segment)
.Symbol("cons-")
.Symbol("voc+")
.Symbol("high+")
.Value;

var rule = new RewriteRule
{
Name = "quantifierInLhs",
// The Lhs is a single top-level Quantifier (an optional segment). The subrule has no Rhs
// (a deletion, 0 children), so the Lhs/Rhs child counts intentionally differ: this steers
// the analysis side to NarrowAnalysisRewriteRuleSpec, which does not cast its Lhs/Rhs
// children eagerly, isolating the crash to SynthesisRewriteRuleSpec, which always validates
// every Lhs child up front regardless of subrule shape.
Lhs = Pattern<Word, ShapeNode>.New().Annotation(highVowel).Optional.Value,
};
Allophonic.PhonologicalRules.Add(rule);
rule.Subrules.Add(new RewriteSubrule());

CompileException ex = Assert.Throws<CompileException>(() => new Morpher(TraceManager, Language));
Exception innermost = Innermost(ex);
Assert.That(innermost, Is.TypeOf<CompileException>());
Assert.That(innermost.Message, Does.Contain("Quantifier"));
}

[Test]
public void QuantifierInReplacementIsRejectedWithClearError()
{
var highVowel = FeatureStruct
.New(Language.PhonologicalFeatureSystem)
.Symbol(HCFeatureSystem.Segment)
.Symbol("cons-")
.Symbol("voc+")
.Symbol("high+")
.Value;
var backRnd = FeatureStruct
.New(Language.PhonologicalFeatureSystem)
.Symbol(HCFeatureSystem.Segment)
.Symbol("back+")
.Symbol("round+")
.Value;

var rule = new RewriteRule
{
Name = "quantifierInRhs",
Lhs = Pattern<Word, ShapeNode>.New().Annotation(highVowel).Value,
};
Allophonic.PhonologicalRules.Add(rule);
rule.Subrules.Add(
// Lhs and Rhs child counts match (1 == 1), so this exercises FeatureAnalysisRewriteRuleSpec,
// which casts both the Lhs and Rhs children eagerly.
new RewriteSubrule { Rhs = Pattern<Word, ShapeNode>.New().Annotation(backRnd).Optional.Value }
);

// Before the fix, this already surfaced as a CompileException at the top level (an unrelated,
// pre-existing catch-all in AnalysisStratumRule wraps *any* exception from compiling a
// phonological rule), but its InnerException was a raw, uninformative InvalidCastException. The
// fix replaces that with a clear, typed CompileException naming the unsupported construct.
CompileException ex = Assert.Throws<CompileException>(() => new Morpher(TraceManager, Language));
Exception innermost = Innermost(ex);
Assert.That(innermost, Is.TypeOf<CompileException>());
Assert.That(innermost.Message, Does.Contain("Quantifier"));
}

private static Exception Innermost(Exception ex)
{
while (ex.InnerException != null)
ex = ex.InnerException;
return ex;
}
}
Loading