Skip to content

LT-22613: fix analysis-length bound over-pruning for count-mismatched rewrite subrules#453

Open
johnml1135 wants to merge 4 commits into
parse-optimizationfrom
lt22613-nogood-cache-fix
Open

LT-22613: fix analysis-length bound over-pruning for count-mismatched rewrite subrules#453
johnml1135 wants to merge 4 commits into
parse-optimizationfrom
lt22613-nogood-cache-fix

Conversation

@johnml1135

@johnml1135 johnml1135 commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes LT-22613: Morpher.ParseWord's default (non-tracing) path returned 0 parses for words a grammar should legitimately parse, while the traced path (which happens to skip this gate) correctly returned 1. The JIRA issue's original write-up hypothesized the cause was AnalysisScope's nogood-memoization cache — that was investigated and disproven (toggling the memo cache independently of the real cause changes nothing); see the corrective comment added to LT-22613 and the corrected analysis doc on bug/csharp-nogood-cache-tracing-divergence (rust/docs/bugs/csharp-nogood-cache-tracing-divergence.md).

Actual root cause: GrammarAnalyzer.ComputeMaxAnalysisLength (Phase 4 "Gate B" of parse-optimization.md) computes a grammar-wide upper bound on analysis-form length, used by AnalysisStratumRule.Apply to prune candidates early — but only on the default, non-tracing path. For any phonological rewrite subrule where Lhs.Count != Rhs.Count, the analysis-side unapplication (EpenthesisAnalysisRewriteRuleSpec/NarrowAnalysisRewriteRuleSpec) never shrinks the candidate shape — it adds new Optional (not Deleted) nodes, which Shape.SegmentCount() still counts. The bound assumed growth of Lhs.Count - Rhs.Count per match site; real growth is always Lhs.Count, regardless of direction. This under-counted the true bound and pruned valid candidates as "too long."

  • 9a43d251 fixes the insertion/expansion case (Rhs.Count > Lhs.Count, epenthesis when Lhs is empty).
  • 93e52dc9 broadens the fix to any count mismatch after an independent adversarial review found the identical bug shape in deletion/coalescence subrules (Lhs.Count > Rhs.Count), and removes the now-unreachable deletion-reapplication compounding logic (and the DeletionReapplications parameter it consumed) as dead code.

Base is parse-optimization (not master): GrammarAnalyzer/AnalysisScope/MaxAnalysisLength only exist on this branch (PR #451, based on hc-rustify PR #446).

Test plan

  • dotnet build — full solution, 0 warnings/errors
  • dotnet test tests/SIL.Machine.Morphology.HermitCrab.Tests — 71/71 passed
  • New regression tests, independently verified red-then-green against each parent commit:
    • ComputeMaxAnalysisLength_ReturnsNull_ForInsertionRewriteRule / ForDeletionRewriteRule (unit-level bound assertions)
    • EpenthesisRuleWithMinimalLexicon (end-to-end minimal-lexicon parse, default vs. traced path)
  • JIRA LT-22613 updated with the corrected root cause and these commit references

🤖 Generated with Claude Code


This change is Reviewable

johnml1135 and others added 4 commits July 10, 2026 15:53
…thesis grammars

Two failing-first tests pinning the bug where Morpher.ParseWord's default
(non-tracing) path returns 0 parses while the traced path returns the correct
1 parse: RewriteRuleTests.EpenthesisRuleWithMinimalLexicon (end-to-end: root
19 "b+ubu" alone in the lexicon, Simultaneous and Iterative epenthesis, so
surface "buibui"/"biubiu" outgrow the auto-derived MaxAnalysisLength bound)
and MorpherTests.ComputeMaxAnalysisLength_ReturnsNull_ForInsertionRewriteRule
(the bound must be inadmissible when an insertion-type rewrite subrule exists).

Isolated mechanism (not the nogood/memo cache -- verified by re-parsing with
the memo active and only MaxAnalysisLength nulled): epenthesis unapplication
marks inserted segments Optional without removing them, so Shape.SegmentCount
at AnalysisStratumRule's Phase 4 Gate B still counts them and the gate prunes
a candidate whose obligatory segments fit the lexicon fine.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e subrules

Fixes the non-tracing/tracing divergence isolated in the previous commit: the
Phase 4 Gate B bound (GrammarAnalyzer.ComputeMaxAnalysisLength) treated
insertion-type rewrite subrules (Rhs longer than Lhs; epenthesis when Lhs is
empty) as contributing nothing, but their ANALYSIS-side unapplication marks
the possibly-rule-inserted surface segments Optional instead of removing them
(EpenthesisAnalysisRewriteRuleSpec / NarrowAnalysisRewriteRuleSpec), so
Shape.SegmentCount at AnalysisStratumRule's gate keeps counting them. A
surface form that legally outgrew the longest root via epenthesis (fixture:
root "b+ubu", surface "buibui", auto-derived bound 4 < 6 segments) was pruned
as unreachable on the default path while the traced path -- which bypasses
the gate -- parsed it correctly. Mode-independent: Iterative epenthesis
over-prunes identically (verified), so the fix keys on subrule shape, not
Simultaneous mode.

Per GrammarAnalyzer's own ground rule (never guess an admissible bound), an
insertion subrule now makes the bound null (gate off), same as compounding or
unmeasurable patterns. The nogood/memo cache (AnalysisScope) was ruled out:
with the memo fully active and only MaxAnalysisLength nulled, the parse is
correct.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Independent adversarial review of the insertion-only fix (9a43d25) found the
same over-prune survives for deletion/coalescence subrules (Lhs longer than
Rhs): NarrowAnalysisRewriteRuleSpec.Unapply always adds Lhs.Count new
(Optional, not Deleted) nodes per match site regardless of Rhs.Count, while
leaving the matched Rhs-shaped region in place (also not Deleted) -- so real
per-site growth is always Lhs.Count, never the Lhs.Count - Rhs.Count this
method budgeted. That undercounts by Rhs.Count whenever Rhs.Count > 0,
independent of which direction the count difference runs.

Broadens the bail-out from "Rhs longer than Lhs" to "counts differ at all",
which makes the now-unreachable deletion-reapplication compounding loop
(and the DeletionReapplications parameter it depended on) dead code; removed
both, along with the doc-comment claim that insertion was uniquely unsound.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CI's "Check formatting" step was failing because several files from the
recent parse-optimization commits weren't run through `dotnet csharpier
format`. No logic changes, just re-wrapping to match the enforced style.

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

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.85714% with 1 line in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (parse-optimization@ccf750e). Learn more about missing BASE report.

Files with missing lines Patch % Lines
....Machine.Morphology.HermitCrab/AnalysisStateKey.cs 0.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@                  Coverage Diff                  @@
##             parse-optimization     #453   +/-   ##
=====================================================
  Coverage                      ?   73.61%           
=====================================================
  Files                         ?      451           
  Lines                         ?    38540           
  Branches                      ?     5357           
=====================================================
  Hits                          ?    28371           
  Misses                        ?     8998           
  Partials                      ?     1171           

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

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.

2 participants