Skip to content

Skip synthesis of candidates whose MPR features or stem names fail - #570

Open
johnml1135 wants to merge 1 commit into
masterfrom
perf/hc-lookup-replay-pr
Open

johnml1135 wants to merge 1 commit into
masterfrom
perf/hc-lookup-replay-pr

Conversation

@johnml1135

@johnml1135 johnml1135 commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

Quick summary

After lexical lookup, Morpher drops an alternative before synthesis when a rule needs a stem name or MPR features its root and trail cannot supply. Real grammars come out about break-even in total work (Aweti +0.07%, Mbugwe 0.00%), conformance and inflection-class fixtures lose slightly (-0.45%, -4.77%), and concord-heavy grammars gain 18%. No parse is lost. Analysis, lookup and public API are untouched.

Where to look

  • MPR bounds -- doc on LookupReplay.IsMprFeasible; LookupReplayMprTests, LookupReplayOutputsTests (Any/Overwrite groups, realizational and compounding outputs, parallel path).
  • Hook after the MaxAlternatives count, per alternative -- Morpher.IsPrunedByLookupReplay.

Deliberately not included

  • No cache and no public switch. Internal LookupReplayMode, LookupReplayVerdict and LookupReplayWork stay because the tests read per-alternative verdicts and work.
  • Template replay: tried, dropped on cost (below).

Validation

  • dotnet test tests/SIL.Machine.Morphology.HermitCrab.Tests -c Release -- 170 passed (origin/master 151)
  • dotnet csharpier check . and pwsh ./scripts/comment-hygiene.ps1 -- clean

Evidence and caveats
  • Shadow run (filter only counts) over the conformance suite (PR Add a conformance suite for HermitCrab, with its adequacy argument #480 merged into a throwaway branch), five sampled grammars and two stress fixtures: 0 successful analyses under a rejected candidate; rejected-candidate steps equal steps saved.
  • Parse signatures on vs off are identical except the truncate-morphotactic* fixtures, which also differ between two runs with the filter off.
  • Pruned candidates no longer emit synthesis rule-not-applied trace events, so a trace consumer sees fewer events for the same parses.
  • The two stress rows are synthetic fixtures kept out of the PR with the census harness. ClassGatedSlots_PruneMostWrongClassCandidates... reproduces the effect as a unit test.
  • Template and final-state replay (tried, dropped, kept at perf/hc-lookup-replay-templates): Mbugwe +1246 saved for 47,560 work, Aweti +2211 for 3728, Sena 118 for 1825; only Amharic paid (745 for 601). Research record: HermitCrab optimization round record: what was tried, and a faster package that is not mergeable #490.
Measured work, in steps

Failure-statistics harness (research/failure-reason-stats), one Release build, serial, 60 s per word, words aborted in any arm dropped. Replay work counts one unit per feature-set match, union or copy; units and steps are not shown to cost the same. Analysis events and lookup candidates are unchanged in every row. Net total-work is (steps saved - replay work) over synthesis steps plus analysis events, counting a unit as a step.

corpus synthesis steps off -> on saved replay work work / saved net total-work parses diff
conformance 8246 -> 8218 28 (0.3%) 189 6.75 -0.45% (gross 0.08%) 4/1476
indonesian 677 -> 677 0 (0.0%) 0 0.00 +0.00% (gross 0.00%) 0/70
amharic 1277 -> 1277 0 (0.0%) 0 0.00 +0.00% (gross 0.00%) 0/24
sena 1908 -> 1908 0 (0.0%) 0 0.00 +0.00% (gross 0.00%) 0/51
aweti 15940 -> 6873 9067 (56.9%) 8043 0.89 +0.07% (gross 0.58%) 0/99
mbugwe 130647 -> 115747 14900 (11.4%) 14887 1.00 +0.00% (gross 1.03%) 0/38
stress-mpr-inflection-classes 494 -> 278 216 (43.7%) 376 1.74 -4.77% (gross 6.45%) 0/52
stress-mpr-bantu-concord 3376 -> 382 2994 (88.7%) 1304 0.44 +17.98% (gross 31.85%) 0/24

The two stress rows are synthetic fixtures not in this PR.

Memory: two MPR feature sets per grammar (realizational and compounding outputs), built at construction, and a clone of the root's set only when a trail entry adds features.

Deferred, and what would unblock it
  • Template replay: needs a verdict reused across the roots of one lookup, which is a cache.

🤖 Generated with Claude Code


This change is Reviewable

After lexical lookup, each alternative is replayed against its root without
shapes and dropped when a rule's recorded allomorph requires MPR features the
root and earlier trail outputs cannot supply, excludes features the root
certainly has, or the rule requires a different stem name. The walk stops at
the first violation, runs only when some rule carries such a constraint, and
keeps nothing between candidates. Family roots, compounding entries and
reachable Overwrite groups disable the unsafe checks. Dropped candidates emit
no synthesis trace events.

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

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.07843% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.00%. Comparing base (c245476) to head (c6e2bbc).

Files with missing lines Patch % Lines
.../SIL.Machine.Morphology.HermitCrab/LookupReplay.cs 95.58% 4 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #570      +/-   ##
==========================================
+ Coverage   74.57%   75.00%   +0.43%     
==========================================
  Files         453      454       +1     
  Lines       38342    38495     +153     
  Branches     5284     5317      +33     
==========================================
+ Hits        28593    28874     +281     
+ Misses       8583     8469     -114     
+ Partials     1166     1152      -14     

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

alternativeCount++;
if (MaxAlternatives > 0 && alternativeCount > MaxAlternatives)
throw new MaxAlternativesExceededException("MaxAlternatives exceeded");
if (IsPrunedByLookupReplay(alternative))

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.

Minor: F1. With tracing on, a pruned candidate leaves an empty trace node. LexicalLookup (Morpher.cs:466) has already emitted SynthesizeWord for it, but the RequiredStemName/RequiredMprFeatures events that explained the failure are gone. Try-a-Word users see a lookup that ends with no reason. LookupReplayMprTests.cs:42-43 confirms it: they count fewer synthesis events with pruning on under IsTracing = true. The precedent is AnalysisStratumRule.cs:142, which skips merging while tracing; skipping pruning the same way would keep traces unchanged. The parallel loop at line 415 needs the same fix.

if (upper == null)
{
work++;
upper = lower.Clone();

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. upper is a new clone of lower plus extra for every trail entry that has required features, even when extra is still the empty _realizationalOut. In that case lower already is the superset. Using it directly saves one allocation and one work unit per entry. That matters here because the PR's own table puts work/saved at about 1.0 for Aweti and Mbugwe.

[Test]
public void NullRecordedAllomorph_IsPrunedOnlyWhenEveryAllomorphFails()
{
// Guessed (null) allomorphs happen when the analysis came from a rule whose allomorph is unconstrained;

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. This comment gives the wrong cause for a null allomorph. AnalysisAffixProcessRule.cs:92 always records _rule.Allomorphs[i], so ParseWord never hands the replay a null allomorph for an affix rule. Nulls come only from GenerateWords/PermuteRules, and that path never calls IsFeasible. The null branches at LookupReplay.cs:145-155 and 167-177 are defensive code. Say that here, so nobody tunes them for a parse case that cannot happen.

@claude

claude Bot commented Oct 27, 2026

Copy link
Copy Markdown
Contributor
  1. Verdict: approve with fixes. The superset and subset bounds hold against SynthesisAffixProcessRule, MprFeatureSet.AddOutput and Word.CheckBlocking as I traced them.
  2. Most important: F1 (Morpher.cs:376). Pruning while tracing removes the failure events that explain a candidate in the trace. Decide whether to skip pruning under IsTracing, as analysis merging already does.
  3. Counts: Critical 0, Important 1, Low 2.
  4. What I ran: gh pr checks 570 shows Build on ubuntu-22.04 and windows-latest passing and Create NuGet package pending. I traced each replay check against its synthesis gate by hand. F1 rests on the PR's own ParseCounting tests, not on a separate reproduction.
  5. Not verified: I did not re-run the measured-work table. The design question stays open: LookupReplay restates the synthesis gates instead of sharing a predicate with them, so a future synthesis constraint can silently invalidate the proofs. The internal LookupReplayMode gives consumers no way to turn pruning off.

Public API, target framework, package, machine.py parity: None verified. Every new member is internal.

Findings: F1 new, F2 new, F3 new.

Reviewed at c6e2bbc

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants