Cap RealizationalRule synthesis application at once per word - #474
Open
johnml1135 wants to merge 1 commit into
Open
Cap RealizationalRule synthesis application at once per word#474johnml1135 wants to merge 1 commit into
johnml1135 wants to merge 1 commit into
Conversation
A Blockable RealizationalRule with no family (or no family member matching the word's current features) reapplied to its own output forever during synthesis: LinearRuleCascade retries a matching rule against its own result whenever that result differs from the input, and SynthesisRealizationalAffixProcessRule never checked an application-count cap the way SynthesisAffixProcessRule and SynthesisCompoundingRule already do. Since RealizationalRule has no multipleApplication attribute, cap it at one application per word. This hung ParseWord/AnalyzeWord confirmation for any root eligible for such a rule, including confirmation of the root's own bare-word parse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #474 +/- ##
==========================================
- Coverage 73.33% 73.32% -0.02%
==========================================
Files 445 445
Lines 37317 37330 +13
Branches 5118 5120 +2
==========================================
+ Hits 27367 27371 +4
- Misses 8825 8834 +9
Partials 1125 1125 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
ddaspit
approved these changes
Aug 13, 2026
ddaspit
left a comment
Contributor
There was a problem hiding this comment.
@ddaspit reviewed 2 files and all commit messages, and made 1 comment.
Reviewable status:complete! all files reviewed, all discussions resolved (waiting on johnml1135).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
SynthesisRealizationalAffixProcessRule.Apply(the synthesis side of aRealizationalRule) never checked a per-word application-count cap theway
SynthesisAffixProcessRuleandSynthesisCompoundingRulealreadydo. That omission let a
BlockableRealizationalRulereapply to itsown output forever whenever
CheckBlockingcouldn't rescue it, hangingParseWord/AnalyzeWord.Reproduction
Minimal case (no
AffixTemplateinvolved — this only reproduces when therule is a direct
Stratummorphological rule withmorphologicalRuleOrder="linear"):PartOfSpeech, oneMorphologicalPhonologicalRuleFeature(mprX).RealizationalRulewhose single subrule requiresmprXand appendsa fixed suffix to whatever it's given.
LexicalEntryon that part of speech carryingruleFeatures="mprX",with no
familyattribute.Parsing that entry's own bare surface form hangs indefinitely. A
stemNameon the entry is not required to reproduce it (an earlier report described
a fixture where the triggering entry happened to carry one) — a bare
ruleFeatures="mprX"entry with no family reproduces it on its own.I found this while investigating a report against a conformance fixture
that has two pre-existing
mprRRealTest-tagged roots and hangs when athird is added. Each of the two pre-existing roots happens to dodge the
bug for a different, unrelated reason: one has
AssignedHeadFeaturesthat conflict with the rule's
RequiredHeadFeatures(so the rule neverapplies at all), and the other has a
familywhose other member doesn'tcarry the same MPR feature (so
CheckBlockingsubstitutes a word thatfails the retry and the recursion stops after one step). A third root
that is rule-eligible and not rescued either way exposes the underlying
defect regardless of
stemName.Root cause
SynthesisStratumRulebuilds its morphological-rule cascade asnew LinearRuleCascade<Word, ShapeNode>(mrules, true, ...). Thetrue("multiple application") tells
LinearRuleCascade.ApplyRulesto retrythe same rule index against a rule's own output rather than only
advancing forward, guarded only by "stop once the rule's output equals
its input" (
LinearRuleCascade.cs, the "avoid infinite loop" comment).For an ordinary
AffixProcessRule/CompoundingRule,SynthesisAffixProcessRule/SynthesisCompoundingRulecheckinput.GetApplicationCount(_rule) >= _rule.MaxApplicationCountbefore doing anything else, so the second attempt at the same rule index
returns nothing and the recursion terminates.
RealizationalRulehas nomultipleApplicationattribute in the DTD and noMaxApplicationCountproperty at all, and
SynthesisRealizationalAffixProcessRule.Applyneverconsulted
GetApplicationCount— even though it already records theapplication via the existing
outWord.MorphologicalRuleApplied(_rule, ...)call. So a root that is realization-eligible and not blocking-rescued keeps
appending the same affix to its own growing output, forever: each pass makes
the shape strictly longer, so the cascade's only other guard never fires
either, and
Morpher.MaxAlternatives(the general escape valve for thisclass of runaway) is wired into
AnalysisStratumRulebut never intoSynthesisStratumRule, so it can't catch this path even when configured.This surfaces during analysis because
Morpher.ParseWordconfirms everyanalysis candidate by re-synthesizing it (
Synthesize), including thetrivial "bare root, no affixes" candidate — so a root eligible for an
unguarded realizational rule hangs confirming even its own bare-word parse.
Fix
Add the same application-count guard
SynthesisAffixProcessRulealreadyhas, hardcoded to a cap of 1 since
RealizationalRulehas no configurablemultipleApplicationattribute — a realizational rule realizes a featurevalue once per word by definition.
Test plan
ParseWord_UnblockedRealizationalRuleMatchesOwnOutput_DoesNotHangto
MorpherTests.cs: aBlockableRealizationalRule(no family) ona stratum with
MorphologicalRuleOrder.Linear, gated by an MPRfeature the root carries. Wraps
ParseWordin a 10s timeout andasserts it completes.
passes with the fix in place.
SIL.Machine.Morphology.HermitCrab.Testssuite: 69/69 passing(68 pre-existing + the new test), including the pre-existing
template-based
RealizationalRulecoverage inAffixTemplateTests.cs.SIL.Machine.Morphology.HermitCrabis referenced only by its owntest project and the
hcCLI tool, so no other test project isaffected by this change.
🤖 Generated with Claude Code
This change is