Skip to content

Find tied containers' descendants regardless of insertion order - #557

Open
johnml1135 wants to merge 1 commit into
masterfrom
fix-554-tied-container-descendants
Open

johnml1135 wants to merge 1 commit into
masterfrom
fix-554-tied-container-descendants

Conversation

@johnml1135

@johnml1135 johnml1135 commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

Quick summary

AnnotationList.FindDepthFirst now tries every container that shares the range of the one Find returned. Previously it descended only into that single container, so a segment inside an equal-range sibling was found or missed depending on insertion order and skip-list levels. The failing-lookup result (nearest descendant) is unchanged.

Where to look

  • AnnotationList.FindDepthFirst and the new private GetSameRange: walks the equal-range group in both list directions, so it does not depend on which tied node Find picks. The change is independent of the BidirList.Find boundary change in Fix directional annotation tie boundaries #556.
  • AnnotationTiedContainerTests: four tied containers, the owner at each position, six fixed skip-list seeds (set by reflection on the test instance only), both search directions. Covers flat ties, nested ties (tied containers inside the owner), and ties that do not own the offset.

Deliberately not included

Validation

  • ./local_check.sh --agent-strict: passed (comment hygiene, csharpier, build, tests).
  • New AnnotationTiedContainerTests: 252 passed, 0 failed with the fix.
  • With only AnnotationList.cs reverted to master: 216 of 252 failed, 36 passed.
  • Not run: this branch combined with Fix directional annotation tie boundaries #556.

Issue context

Related: #506, #500, #555, #556.

Fixes #554

🤖 Generated with Claude Code


This change is Reviewable

AnnotationList.FindDepthFirst only descended into the single container that
Find returned. When several containers share a range and only one owns the
children, the result depended on insertion order and skip-list levels. Try
every container with the same range as the one Find returned.

Fixes #554

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

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.20690% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.58%. Comparing base (74ee09b) to head (04b7986).

Files with missing lines Patch % Lines
src/SIL.Machine/Annotations/AnnotationList.cs 86.20% 3 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #557      +/-   ##
==========================================
+ Coverage   74.57%   74.58%   +0.01%     
==========================================
  Files         453      453              
  Lines       38314    38341      +27     
  Branches     5280     5285       +5     
==========================================
+ Hits        28573    28597      +24     
- Misses       8576     8579       +3     
  Partials     1165     1165              

☔ 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.

}

if (!candidate.IsLeaf && candidate.Range.Contains(offset, dir))
return candidate.Children.FindDepthFirst(offset, dir, out result);

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.

FYI: F1. On a miss, result still comes only from candidate, so the start that Matcher derives from it (Matcher.cs:329-334) still depends on which tie sorts last. Ties A (owns n0) and B (owns n3), offset n2: with B as candidate, the result is B's Begin, so Matcher starts at n3. With A as candidate, the result is n0, so Matcher starts at A's End. No test covers the miss path. Unverified: traced, not run.

}
}

if (!candidate.IsLeaf && candidate.Range.Contains(offset, dir))

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.

FYI: F2. On a miss, candidate's subtree is searched twice: once in the loop, because GetSameRange yields it first, and again here. The cost compounds across nested failing levels: about (ties + 1)^depth searches, where the old code did one per level. Keep the loop's inner for candidate and return that instead.

// Several containers can share the same range while only one of them owns the children that lead to the
// offset, so every one of them has to be tried, not just the one Find happened to return.
Annotation<TOffset> candidate = result;
foreach (Annotation<TOffset> tied in GetSameRange(candidate))

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.

FYI: F3. Only containers with exactly the same range are tried, so an earlier sibling with a wider range that owns the offset is still missed. Example: A[n0,n3] owns n0..n3 and B[n0,n2] is a leaf, both added with subsume: false. The list order is A, B, Find(n1) returns B, and FindDepthFirst(n1) returns false. This is pre-existing and deterministic. Traced, not run.

{
var annotations = new AnnotationList<ShapeNode>();
typeof(BidirList<Annotation<ShapeNode>>)
.GetField("_rand", BindingFlags.Instance | BindingFlags.NonPublic)!

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.

FYI: F4. Because _rand is set by reflection, renaming that private field would make all 252 cases fail with a NullReferenceException from the !. SIL.Machine already declares InternalsVisibleTo("SIL.Machine.Tests"), so an internal seed constructor on BidirList would give the tests a typed hook instead.

@claude

claude Bot commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

Verdict: approve. The hit path now tries every container with the same range, and the new tests cover each owner position, seed and direction.

Most important: F1 (src/SIL.Machine/Annotations/AnnotationList.cs:247). When the lookup misses, result still comes only from the container Find returned, and no test covers that path.

Counts: 0 Critical, 0 Important, 4 Low.

Ran: /code-review high 557, then traced its candidates through BidirList.Find, Annotation.CompareTo and Matcher.GetStartAnnotation. Of seven candidates, four are posted. CI per gh pr checks 557: comment hygiene passed, while Build on ubuntu-22.04 and windows-latest is still pending.

Not verified: F1 and F3 were traced, not run, because this session could not write a scratch test. I did not run this branch combined with #556.

Public API, target framework, package, machine.py parity: None verified. FindDepthFirst keeps its signature, and the only change in behavior is the hit for tied containers.

F1 new, unverified. F2 new. F3 new, unverified. F4 new.

Reviewed at 04b7986

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.

AnnotationList.FindDepthFirst misses descendants in tied containers depending on insertion order

2 participants