Skip to content

fix(hermitcrab): make metathesis switch-name order not matter - #471

Open
johnml1135 wants to merge 4 commits into
masterfrom
fix/metathesis-morph-annotation-sort
Open

fix(hermitcrab): make metathesis switch-name order not matter#471
johnml1135 wants to merge 4 commits into
masterfrom
fix/metathesis-morph-annotation-sort

Conversation

@johnml1135

@johnml1135 johnml1135 commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Problem

A MetathesisRule whose leftSwitch names the earlier of its two pattern groups throws instead of producing an analysis:

System.InvalidOperationException: Failed to compare two elements in the array.
 ---> System.ArgumentException: Only nodes from the same list can be compared. (Parameter 'other')
   at SIL.Machine.Annotations.ShapeNode.CompareTo(ShapeNode other)
   at SIL.Machine.Annotations.ShapeRangeFactory.Compare(ShapeNode x, ShapeNode y)
   at SIL.Machine.Annotations.Range`1.CompareTo(Range`1 other)
   at System.Linq.Enumerable.EnumerableSorter`2...
   at SIL.Machine.Morphology.HermitCrab.Morpher.Synthesize(...)

Both metathesis specs silently required the opposite orientation. Every pre-existing test in MetathesisRuleTests names the later group as the left switch, so the intuitive order was never exercised.

Direction is not involved — this reproduces under both LeftToRight and RightToLeft.

Cause

MoveNodesAfter advances its cur anchor after every iterated node, whether or not that node was physically moved. In the failing orientation beforeRightGroup ends up equal to leftGroup.Range.End, so for a single-node group that node is simultaneously the move's anchor and the node being removed in the same iteration. node.Remove() sets its List to null and the following cur.AddAfter(node) never restores it. The orphaned node then reaches morph.Children.OrderBy(ann => ann.Range), and ShapeNode.CompareTo throws because the two nodes belong to different lists.

Separately, AnalysisMetathesisRuleSpec builds its pattern in leftSwitch-then-rightSwitch order, but that pattern must match the surface, where the two groups appear in the opposite order from the underlying form. With the switch names reversed the analysis pattern matched nothing — so even with the crash fixed there was still no analysis.

Fix

Both specs order the two switch groups by pattern position rather than by name, so a rule behaves identically whichever way its switch names are written.

For rules that already name the later group first the normalization is a no-op: the swap condition is false, and AnalysisMetathesisRuleSpec emits the same order it always did.

Verification

Full SIL.Machine.Morphology.HermitCrab.Tests suite: 71/71 pass. dotnet csharpier check clean.

Each half of the fix was reverted independently to confirm both are load-bearing:

Reverted Result
synthesis normalization SimpleRule_LeftSwitchNamesEarlierGroup and its right-to-left variant reproduce the reported crash
analysis normalization all three new tests fail with an empty analysis instead
neither 71/71 pass

Tests added:

  • SimpleRule_LeftSwitchNamesEarlierGroup — differs from SimpleRule only in the switch-name order, and must give the same result
  • SimpleRule_LeftSwitchNamesEarlierGroup_RightToLeft — same, with Direction.RightToLeft
  • ComplexRule_LeftSwitchNamesEarlierGroup — reversed naming with a group between the two switches. Note this one passes even without the synthesis change; it guards the analysis-side ordering and the middle-group case rather than reproducing the crash.

Scope, and what this does not close

This fixes the switch-name-order crash class. Two pre-existing hazards in the same code are left untouched and are not claimed to be fixed:

  • MoveNodesAfter still advances cur past a skipped non-Segment node, so a switch group whose own range spans a boundary or anchor node could still anchor subsequent moves off a stale position. No current rule shape hits this.
  • An unmatched switch capture has a null Range.Start. beforeRightGroup's .Prev already dereferenced it before this change; the new comparison is guarded with GroupCapture.Success so it does not add a second such site, but the underlying assumption that both switches always match is unchanged.

How it was found

While building generated-coverage fixtures for the HermitCrab XML surface on the conformance-framework branch: a fixture written to exercise MetathesisRule@multipleApplicationOrder wrote its switch names in the natural order and hit this. The ordering attribute turned out to be irrelevant.

🤖 Generated with Claude Code


This change is Reviewable

A MetathesisRule whose leftSwitch names the EARLIER of the two pattern groups threw
InvalidOperationException "Failed to compare two elements in the array", wrapping
ArgumentException "Only nodes from the same list can be compared", instead of producing
an analysis.

Both metathesis specs silently required the opposite orientation, and every existing
test names the later group as the left switch, so the intuitive order was never
exercised.

SynthesisMetathesisRuleSpec.ApplyRhs splices by moving one group's nodes out to the
right and then moving the other into the gap. That only works when the group it is
handed first is the LATER of the two in shape order. Handed them the other way round,
the second move re-anchors a group after its own end, which detaches nodes and leaves
child annotation ranges spanning two lists; the subsequent OrderBy over those ranges is
what throws.

AnalysisMetathesisRuleSpec builds its pattern in leftSwitch-then-rightSwitch order, but
that pattern has to match the SURFACE, where the two groups appear in the opposite order
from the underlying form. With the switch names the other way round the analysis pattern
matched nothing, so even without the crash there was no analysis.

Both now order by pattern position rather than by name, so a rule behaves identically
whichever way its switch names are written. Rules that already named the later group
first are unaffected: for them the normalization is a no-op.

Direction is NOT involved; this reproduces under both LeftToRight and RightToLeft.

The added test differs from SimpleRule only in swapping the two switch names, and must
produce the same result. Verified that reverting either half alone reintroduces a
distinct failure: without the synthesis change the crash returns, and without the
analysis change the word gets no analysis.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@codecov-commenter

codecov-commenter commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.66667% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 73.34%. Comparing base (0b58830) to head (01add43).

Files with missing lines Patch % Lines
...b/PhonologicalRules/SynthesisMetathesisRuleSpec.cs 83.33% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##           master     #471   +/-   ##
=======================================
  Coverage   73.33%   73.34%           
=======================================
  Files         445      445           
  Lines       37317    37327   +10     
  Branches     5118     5121    +3     
=======================================
+ Hits        27367    27377   +10     
  Misses       8825     8825           
  Partials     1125     1125           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

johnml1135 and others added 3 commits August 12, 2026 07:08
Rationale, history and the reproduction belong in the PR body, not in the source.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…sked for

A middle-group case with the switch names reversed, a right-to-left variant, and a
Success guard so the new comparison does not dereference an unmatched capture.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…one line

Keeps the load-bearing ordering rationale; drops the extra sentence about
unmatched captures, which the adjacent Success checks already make plain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@ddaspit ddaspit left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:lgtm:

@ddaspit reviewed 3 files and all commit messages, and made 1 comment.
Reviewable status: :shipit: complete! all files reviewed, all discussions resolved (waiting on johnml1135).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants