Cost-Reuse: Fix latent classification bugs and code cleanup#3890
Merged
Conversation
…g them stable
A @StableCost feature that is a composite is treated as "transparent": the
locality walk recurses into its children and it stays reusable only if they all
are. But CostReuse.follow descended only into Feature and TermGenerator children
-- ProjectionToTerm (and TermFeature) were silently skipped. So ApplyTFFeature,
whose cost is termFeature.compute(proj.toTerm(...)), was classified stable
unconditionally, even though ProjectionToTerm.toTerm receives the goal and can
therefore read volatile proof state. Nine @StableCost features share this shape
(ApplyTFFeature, LetFeature, InstantiatedSVFeature and the polynomial/monomial
family). It is currently harmless -- no ProjectionToTerm in the tree reads
volatile state -- but a future goal-reading projection dropped into one of them
would be silently mis-classified, letting the strategy reuse a stale cost (never
unsound, but it can stall the search).
Make the walk treat ProjectionToTerm as a first-class classified node, recursing
into it exactly like a child feature (so composite projections are validated
too); an unannotated projection is volatile, the safe default.
Classify the twelve concrete projections by what their toTerm actually reads:
- ten @StableCost: they derive their term only from the rule application /
match / find subterm and the stable Services (the two SV/trigger instantiation
projections, the pinned-\assumes projection, the term buffer, and the
transparent subterm/term-construction/monomial/polynomial projections);
- two @WeakStableCost: FocusFormulaProjection returns the whole find formula,
and FocusProjection with stepsUpwards > 0 (e.g. FocusProjection.create(1))
walks up from the find subterm -- both read above the subterm, so their cost
is reusable only while the find formula is unchanged.
Annotations are not inherited, so the two concrete subclasses of the abstract
divide-projection are annotated directly; allFields still walks their inherited
sub-projection fields. TermFeature stays untraversed: its compute() has no goal,
so it cannot read volatile state.
No behaviour change on the current tree (every feature keeps its classification);
this closes the latent gap and gives the two focus projections their correct
weak-stable locality.
Created with AI tooling support
CostReuse dispatched on `o instanceof Feature || o instanceof TermGenerator || o instanceof ProjectionToTerm` and read the @StableCost / @WeakStableCost / @VolatileCost annotations inline, with term generators handled specially (annotation only, no recursion). Introduce a common interface CostClassifiable, extended by Feature, TermGenerator and ProjectionToTerm -- exactly the cost components that receive the goal and whose cost may therefore be volatile. TermFeature has no goal and is deliberately excluded (stable by construction). The interface carries the reuse-locality as an annotation-driven default method locality() returning the new CostLocality enum. CostReuse becomes uniform: follow routes any CostClassifiable, and walk recurses into every child component the same way (kind() collapses to localityOf() over a CostLocality cache; the NonDuplicateApp veto stays a single instanceof, since it is an application guard, not a locality). The isLocal helper and the term- generator special case are gone. Besides removing the instanceof chain, this closes a residual gap: term generators are now recursed like every other component, so a ProjectionToTerm held by a generator (SubtermGenerator, MultiplesModEquationsGenerator, RootsGenerator) is validated instead of silently skipped. Created with AI tooling support
unp1
marked this pull request as ready for review
July 5, 2026 11:36
unp1
enabled auto-merge
July 5, 2026 13:55
wadoon
approved these changes
Jul 5, 2026
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.
Related Issue
Follow-up to #3873 (cost-reuse follow-up fixes), itself a follow-up to #3837. No separate issue.
Intended Change
The cost-reuse locality classifier (
CostReuse) decides which taclet costs may be reusedunchanged as the proof grows. For a
@StableCostcomposite feature the annotation means"transparent": the walk recurses into the feature's children and it stays reusable only if
they are all local too. However,
CostReuse.followdescended only intoFeatureandTermGeneratorchildren —ProjectionToTermandTermFeaturewere silently skipped.As a result a feature such as
ApplyTFFeature, whose cost istermFeature.compute(proj.toTerm(...)), was classified stable unconditionally, eventhough
ProjectionToTerm.toTerm(app, pos, goal, mState)receives the goal and can thereforeread volatile proof state. Nine
@StableCostfeatures share this shape (ApplyTFFeature,LetFeature,InstantiatedSVFeature, and the polynomial/monomial family).This is currently harmless — no
ProjectionToTermin the tree reads volatile goal state(all only fetch the stable
Services; the genuine goal-readersInstantiationCostandIntroducedSymbolByareFeatures, correctly left unmarked → volatile) — but it is a latentgap: a future goal-reading projection dropped into one of these composites would be silently
mis-classified, letting the strategy reuse a stale cost. As always for costs, that can never
make a proof unsound; it can only perturb (or stall) the heuristic search.
This PR closes the gap:
CostReusenow treatsProjectionToTermas a first-class classified node —followroutesit and the walk recurses into it exactly like a child feature, so composite projections
(
Subterm,TermConstruction,CoeffGcd,AbstractDividePolynomials,ReduceMonomials)are validated too. An unannotated projection is treated as volatile — the safe default.
The twelve concrete
ProjectionToTermimplementations are annotated by what theirtoTermactually reads:
@StableCost— the two SV/trigger-instantiation projections, the pinned-\assumesprojection, the term buffer, and the transparent subterm / term-construction / monomial /
polynomial projections: each derives its term only from the rule application / match / find
subterm and the stable
Services.@WeakStableCost—FocusFormulaProjectionreturns the whole find formula, andFocusProjectionwithstepsUpwards > 0(e.g.FocusProjection.create(1)) walks up fromthe find subterm. Both read above the subterm, so their cost is reusable only while the
find formula is unchanged.
(Annotations are not inherited, so the two concrete subclasses of the abstract divide-projection
—
DividePolynomialsProjection,MonomialColumnOp— are annotated directly;allFieldsstillwalks their inherited sub-projection fields.)
TermFeatureis deliberately left untraversed and documented as stable by interface: itscompute(term, mState, services)has nogoal, so it cannot read volatile state.Classification changes only for taclets scored through the two focus projections: they move
from
StableCosttoWeakStableCost, i.e. their cost is now recomputed when the find formulachanges instead of being reused unconditionally. Because the baseline VERIFY run is clean (those
costs did not in fact vary), the recomputed value equals the previously-reused one, so proof
search — and node counts — are expected to be identical to main; the only effect is a little
extra cost recomputation for those taclets. This must be confirmed by the validation runs below
(it is no longer neutral purely by construction). Everything else keeps its classification.
A possible later refinement: split
FocusProjectionso the commoncreate(0)(find subterm,genuinely stable) keeps
StableCostand only the upward variant is weak-stable. Left out here tokeep the change minimal and conservative.
Second change: a common
CostClassifiableinterface (commitd29b748b66)With the classifier now recursing into projections, its dispatch was still an
o instanceof Feature || o instanceof TermGenerator || o instanceof ProjectionToTermchain plusinline annotation reading, with term generators handled specially (annotation only, no recursion).
This commit unifies that:
CostClassifiable(inncore.calculus.costbased) is extended byFeature,TermGeneratorandProjectionToTerm— exactly the three components that receive the goal andcan therefore be volatile.
TermFeaturestays out (itscomputehas no goal → stable byconstruction). It carries the reuse-locality as an annotation-driven default
locality()returning a new
CostLocalityenum (STABLE/WEAK_STABLE/VOLATILE).CostReusebecomes uniform:followroutes anyCostClassifiable, andwalkrecurses intoevery child the same way (
kind()collapses tolocalityOf()over aCostLocalitycache). Theonly remaining
instanceofis theNonDuplicateAppveto — which is an application guard,not a locality, so its being special is honest. The
isLocalhelper and the term-generatorbranch are gone.
ProjectionToTermheld by a generator (SubtermGenerator,MultiplesModEquationsGenerator,RootsGenerator) is validated instead of silently skipped — a small gap the first commit left.This is an additional behavioural surface the validation runs below must cover.
Plan
ProjectionToTerm(recurse into composite projections)@StableCost, 2@WeakStableCost)Feature/TermGenerator/ProjectionToTermunderCostClassifiable; make theCostReusewalk uniform (removes theinstanceofchain; recurses generators' projections)key.corecompiles-Dkey.strategy.costReuse.verify= 0 mismatches over the RAP corpus (the same gate Follow-up fixes for cost-reuse: remove unsound static caches #3873 used)spotlessApplypass over the touched filesType of pull request
Ensuring quality
verification harness (
-Dkey.strategy.costReuse.verify), which recomputes every reused costand warns on any mismatch; this PR must show 0 mismatches over the RAP corpus before merge
(pending — see Plan). No new unit test is added: the classifier's contract is checked
corpus-wide by that harness rather than by isolated cases.
unchanged for every existing feature, so cost reuse is neither gained nor lost on the current
rule base.
Additional information and contact(s)
Created with AI tooling support.
Spotted during a review of the
@StableCostannotations:ApplyTFFeatureis marked stable atthe class level although its cost depends on the
ProjectionToTermit wraps. The audit thatfollowed showed the same pattern in nine composites and confirmed it is dormant today, which is
why this is a robustness/refactoring change rather than a bug fix.
The contributions within this pull request are licensed under GPLv2 (only) for inclusion in KeY.