Skip to content

Term label cleanup: simpler framework, label-agnostic term equality#3884

Draft
unp1 wants to merge 14 commits into
mainfrom
termlabel-cleanup
Draft

Term label cleanup: simpler framework, label-agnostic term equality#3884
unp1 wants to merge 14 commits into
mainfrom
termlabel-cleanup

Conversation

@unp1

@unp1 unp1 commented Jul 3, 2026

Copy link
Copy Markdown
Member

In one paragraph: the term-label subsystem carried a lot of generality that was designed up-front but never actually used, and its central design choice, term equals/hashCode being label-sensitive, forced a comparison-modulo-labels idiom to spread across the code base. This PR removes the unused machinery, flips term equality to ignore labels by default (the common and now-uniform case), restores the few places that genuinely need label-sensitive comparison via a small explicit apparatus, and simplifies TermLabelManager and the hook API. Proofs are unaffected: replaying a 17-proof corpus yields byte-identical sequents including every term label, and the information-flow and well-definedness proof suites match stock master.

Main changes and intentions

  1. Remove speculative-but-unused features. Several extension points were anticipated but never materialized in any profile:

    • the ChildTermLabelPolicy hook family (direct + child/grandchild) — registered by nobody;
    • the APPLICATION_DIRECT_CHILDREN refactoring scope — returned by no refactoring;
    • the LabeledTermImpl subclass — folded into TermImpl.
  2. Change the semantics of term equality: equals/hashCode now ignore all term labels. Term labels are, by contract, not soundness relevant, and sequent redundancy elimination already ignored them — so the default now agrees with the calculus' own notion of "same formula". The two label-sensitive needs that remain are served explicitly: IRRELEVANT_TERM_LABELS_PROPERTY (ignore only cosmetic labels, keep proof-relevant ones) and equalsIncludingLabels/labeledHashCode (respect all labels, used only by term interning / index caches). Rationale and the full pro/contra discussion are in the table below.

  3. Simplify TermLabelManager and the hook API. TermLabelManager shrinks from 2211 to 1629 lines. The hooks (TermLabelPolicy/Update/Refactoring) no longer thread an 8-parameter rule-application tuple through every signature and every internal delegation; a single immutable TermLabelContext record carries it.

Net: 40 files, +1015 / −1691 (−676 lines), Java only, no taclet changes.

Related Issue

Addresses the two long-standing complaints about the term-label implementation: the
framework being too general/partly inefficient, and the proliferation of
equalsModIrrelevantTermLabels comparisons.

Intended Change

(A) Prune unused generality

  • ChildTermLabelPolicy (and its direct / child-and-grandchild variants) is deleted together with its dispatch in TermLabelManager. No TermLabelConfiguration in any profile ever registered one.
  • RefactoringScope.APPLICATION_DIRECT_CHILDREN and its RefactoringsContainer bucket / dispatch are deleted; no live refactoring returns that scope.
  • LabeledTermImpl is merged into TermImpl. With labels no longer part of term identity, the subclass existed only to keep a labels field off unlabeled terms; a single nullable field (empty normalized to a shared array) replaces it, removing the two-class hierarchy and the "same value, different class, still equal" hazard.

(B) Term equality ignores labels

TermImpl.equals/hashCode now ignore all term labels. Three comparison modes now coexist, cleanly separated by who owns the knowledge:

Mode API Knows about label kinds? Used for
ignore all labels t1.equals(t2) / hashCode no — labels are opaque default: logical identity ("same formula")
ignore only cosmetic labels equalsModProperty(t, IRRELEVANT_TERM_LABELS_PROPERTY) yes — consults isProofRelevant() the ~51 sites that must keep, e.g., an anonHeapFunction-labeled term distinct while ignoring origin labels
respect all labels equalsIncludingLabels + labeledHashCode no — labels are opaque term-factory interning and taclet-index caches (StrictTermKey)

Supporting apparatus (all label-opaque, hence on TermImpl, not leaking label kinds): - equalsIncludingLabels(Object) and cached labeledHashCode() — the label-sensitive counterparts of equals/hashCode. labeledHashCode is cached on the term because hashCodeModProperty is not and the term factory cache queries it per construction. - StrictTermKey — a label-sensitive map key. The TermFactory intern cache and the taclet-app index CacheKey use it, so labeled terms are still interned exactly as before (memory sharing and == fast paths preserved), even though equals ignores labels. Taclet matching can depend on labels, so the index must stay label-sensitive. - A cached containsLabelsRecursive() guard so label-free subtrees are skipped in O(1).

The four TERM_LABELS_PROPERTY comparison sites (which meant "ignore all labels") are now plain equals; that property is no longer used in application code.

(C) API simplification

  • TermLabelContext: an immutable record bundling the rule application (state, services, applicationPosInOccurrence, applicationTerm, modalityTerm, rule, ruleApp, goal, hint, tacletTerm), built once per TermLabelManager entry point. Hooks now take (context, payload…): keepLabel went 10→3 params, updateLabels 11→3, defineRefactoringScope 8→1, refactorLabels 10→3. 26 manager methods that re-threaded the tuple collapse to one-argument delegation. TermLabelMerger is unchanged (its parameters are genuine payload). The static entry points keep their signatures, so rule implementations are untouched.

Why "ignore all" and not "keep the old label-sensitive equals": pro / contra

The decision (which default equals should have) was deliberate. Both candidates are label-opaque (neither leaks specific label knowledge into Term).

equals includes all labels (old) equals ignores all labels (this PR)
conceptual contract implementation identity ("same content incl. annotations") logical identity ("same formula") — consistent with labels must not be soundness relevant
consistency with sequents inconsistent: sequent membership ignores labels but equals does not (the "rejected as duplicate yet not equal to any member" trap) aligned — sequent redundancy already ignores labels
opt-out burden ~55 sites must weaken to ignore labels; proof-relevance leaks widely 4 infrastructural sites must strengthen (the interning caches), all in label-maintenance code
new-code failure mode a naive equals/map breaks once origin labels are enabled (the historical bug pattern) a naive term-keyed map silently merges label variants → labels dropped; bounded — never unsound, only lost annotations
extra machinery none equalsIncludingLabels + cached labeledHashCode + StrictTermKey (small, and interning is preserved)
perf baseline parity on the cached-construction hot path (measured); equals marginally faster on labeled terms

The deciding factor is that the proliferation of comparison-modulo-labels only has an exit under "ignore all": under the old default, plain equality is unusable the moment origin labels are on, so the weakening idiom is structurally forced everywhere. Under this PR the weakening residue is the ~51 IRRELEVANT_TERM_LABELS_PROPERTY sites that genuinely need the middle mode (a per-site review confirmed each: emptiness/constant classification in the loop/contract rules and WD, instantiation identity in SVInstantiations/RuleCommand, and a few non-soundness strategy heuristics).

Plan

Finished at submission time.

  • Remove unused ChildTermLabelPolicy hook family
  • Make term equality label-agnostic; add equalsIncludingLabels
  • Add term-label equivalence regression harness
  • Merge LabeledTermImpl into TermImpl
  • Intern labeled terms via a label-sensitive cache key (StrictTermKey)
  • Update TestLinkedHashMapWrapper for label-agnostic equality
  • Short-circuit labelsEqualRecursive on reference-identical subterms (perf)
  • Pack TermImpl lazy tri-state caches into a single int (−12 bytes/term)
  • Collapse TERM_LABELS_PROPERTY comparisons to plain equals
  • Remove unused APPLICATION_DIRECT_CHILDREN refactoring scope
  • Rewrite term-label API documentation
  • Introduce TermLabelContext parameter object

Type of pull request

  • Refactoring (behaviour should not change or only minimally change)
  • Breaking change (API: equals/hashCode semantics of terms, and the TermLabelPolicy/Update/Refactoring hook signatures — relevant only to code implementing those hooks or relying on label-sensitive term equality)
  • There are changes to the (Java) code

Ensuring quality

  • I made sure that introduced/changed code is well documented (javadoc and inline comments); the equality contract and the fixed hook execution order are documented on TermLabel, the hook interfaces, and TermLabelManager.
  • I made sure that end-user/developer docs are updated: companion PR in https://github.com/KeYProject/key-docs adds a "Term Labels" internals page (concept → design → hook taxonomy) and an "Add a Term Label" how-to.
  • I added new test case(s): a term-label equivalence regression harness (TermLabelEquivalenceDumpTest) that replays console proofs and dumps every node's sequent including all labels; plus label-interning and equality-contract unit tests.
  • I have tested the feature as follows: the equivalence harness (17 proofs, 23 373 nodes) is byte-identical including labels across every commit; :key.core:test, :key.core.infflow:testRunAllInfProofs, and :key.core.wd:testRunAllWdProofs are green and match stock master.
  • I have checked that runtime performance has not deteriorated: a construction/equals/hashCode micro-benchmark against stock master shows parity on the cached-construction hot path (the production path); labeled-term equals is marginally faster. The flag packing saves ~12 bytes per term object.

Additional information

@flo2702 thanks a lot for offering to look into this PR

  • The equality change is behavior-preserving for proofs but is a semantic change for any code that keyed a Map/Set on JTerm expecting label sensitivity; such code should wrap keys in StrictTermKey. The (deprecated) key.core.symbolic_execution and key.core.proof_references modules were left untouched and pass their proof suites.
  • TermLabelsProperty (mode "ignore all") is now equivalent to plain equals and unused in application code; it is retained only for tests and can be removed in a follow-up.

The contributions within this pull request are licensed under GPLv2 (only) for inclusion in KeY.

unp1 and others added 13 commits July 3, 2026 12:39
No profile ever registers a ChildTermLabelPolicy (neither direct child nor
child-and-grandchild variant). Delete the interface, the four policy maps and
their dispatch in TermLabelManager, the corresponding TermLabelConfiguration
parameters, and the tests exercising the dead hooks.

Generated with AI tooling support
…ct cases

Term labels are not soundness-relevant and sequent membership already ignores
them (isRedundant uses RENAMING_TERM_PROPERTY). Yet TermImpl.equals/hashCode
were label-sensitive, forcing ~74 call sites to opt out via
equalsModProperty(IRRELEVANT_TERM_LABELS/TERM_LABELS). Flip the default:

- TermImpl.equals/hashCode now ignore all term labels; LabeledTermImpl drops
  its equals/computeHashCode overrides.
- Add JTerm.equalsIncludingLabels (label-sensitive, sets per subterm) plus a
  cached containsLabelsRecursive() guard on TermImpl.
- Protect the few caches whose correctness needs label distinction: the
  TermFactory intern cache now skips label-carrying terms; the taclet-app
  index CacheKey and the origin-subterm cache (via new StrictTermKey) use
  equalsIncludingLabels. TermLabelManager refactoring change-detection uses
  equalsIncludingLabels.

Collapsing the redundant equalsModProperty call sites follows separately.

Generated with AI tooling support
Replays 17 console-style KeY proofs (~23k nodes) and serializes every proof
node's sequent including all term labels at every subterm, to the directory
given by -Dkey.labeldump.dir. Diffing the output across two revisions proves a
change did not alter any sequent or label. Asserts coverage of the six term
labels actually attached during standard proof search (origin, anonHeapFunction,
selectSK, impl, SC, loopScopeIndex).

Generated with AI tooling support
With term labels no longer part of term identity, the LabeledTermImpl subclass
only served to keep a labels field off unlabeled terms. Fold it into TermImpl:
a single nullable labels field (empty normalized to the shared EMPTY_LABEL_LIST),
label accessors and label-aware toString moved up, and TermFactory now always
constructs TermImpl. This removes the two-class hierarchy and the 'same value,
different class, still equal' hazard the equality flip introduced.

Verified behavior-preserving: replaying 17 console proofs (23373 nodes) yields
byte-identical sequents including every term label at every subterm.

Generated with AI tooling support
The equality flip left the TermFactory cache keyed on the now label-agnostic
term equality, which would have conflated label variants; the interim fix
excluded label-carrying terms from the cache entirely. That is a regression:
origin labels alone are attached to a large fraction of terms, so the cache
would go largely unused, term interning (and the == fast paths that rely on it)
would stop firing, and memory would grow through duplicate labeled terms.

Instead key the cache on StrictTermKey (label-sensitive equality) and cache all
terms again. This reproduces exactly the interning behaviour from when term
equality was itself label-sensitive, while keeping term.equals() label-agnostic
for the logic layer. Add JTerm.labeledHashCode() (cached, label-sensitive) so
label variants keep distinct hash buckets.

Generated with AI tooling support
The plain LinkedHashMap baseline in testTermLabelProperties encoded the old
label-sensitive term equality; under the flip a plain map treats label variants
as the same key. Adjust the basicMap assertions accordingly (wrapper-map
assertions are unaffected as they compare via a Property).

Generated with AI tooling support
equalsIncludingLabels walks the tree once for structural equality and once for
labels. Because the term factory interns terms label-sensitively, structurally
shared subterms are reference-identical, so add a t1 == t2 fast path to the
label walk. This restores cached labeled-term construction to parity with the
pre-flip factory (a synthetic all-labeled depth-15 tree went 6.3ms -> 2.4ms,
matching stock main's 2.3ms); equals on labeled terms is now marginally faster
than stock.

Generated with AI tooling support
Replace the four ThreeValuedTruth reference fields (rigid, containsJavaBlock/
Transformer/LabelsRecursive) with one packed int (two bits each), saving ~12
bytes per term object. Writes go through an AtomicIntegerFieldUpdater CAS that
only sets a still-UNKNOWN flag, so concurrent computation of different flags on
the same shared, immutable term cannot lose updates (the previous separate
reference fields were independently written; a shared word needs CAS). The
field is volatile; reads are plain volatile int loads.

Generated with AI tooling support
TermLabelsProperty ignores all term labels, which is exactly what the (now
label-agnostic) default term equality does. Replace the four
equalsModProperty(TERM_LABELS_PROPERTY) / equalsModThisProperty call sites with
plain equals() and drop the dead imports. (The TermLabelsProperty class is kept
for now; its removal is bundled with the IRRELEVANT_TERM_LABELS_PROPERTY review.)

Generated with AI tooling support
No term label refactoring ever returns this scope (verified across all
modules). Delete the enum value, the directChildRefactorings bucket in
RefactoringsContainer, the refactorChildTerms pass, and the framework tests
exercising the dead scope.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n are infflow-attached

These labels are attached by key.core.infflow (InfFlowBlockContract/
WhileInvariantRule, PrepareInfFlowContractPreBranchesMacro,
InfFlowInputOutputRelationSnippet), not dead as the harness comment claimed;
they are simply unreachable from standard Java profile replays and covered by
testRunAllInfProofs instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Document the new equality contract on TermLabel (equals ignores all labels;
equalsIncludingLabels/labeledHashCode strict; IRRELEVANT property in between),
make the Policy-before-Update execution order an explicit documented contract
on both hook interfaces and on TermLabelManager, state on TermLabelsProperty
that it is now equivalent to plain equals for JTerms, and describe on
IrrelevantTermLabelsProperty the three comparison modes and why label-kind
knowledge lives in a Property rather than on the term.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The term label hooks dragged the identical 8-tuple (state, services,
applicationPosInOccurrence, applicationTerm, rule, goal/ruleApp, hint,
tacletTerm) through every signature: keepLabel took 10 parameters,
updateLabels 11, and 26 TermLabelManager methods re-threaded the tuple hop by
hop. Bundle the rule application into an immutable TermLabelContext record,
constructed once per manager entry point; hooks now receive (context,
payload...) only:

  TermLabel keepLabel(TermLabelContext, JTerm sourceTerm, JTerm newTerm, TermLabel)
  void updateLabels(TermLabelContext, JTerm newTerm, Set<TermLabel>)
  RefactoringScope defineRefactoringScope(TermLabelContext)
  void refactorLabels(TermLabelContext, JTerm term, LabelCollection)

The policy signature keeps the source term explicit because modality-term
policies are invoked with the modality term, not the application term (this
also documents that so-far implicit contract). The context carries both goal
and ruleApp (nullable), removing the arbitrary asymmetry where policies got
only the former and updates only the latter. TermLabelMerger is unchanged
(its parameters are all payload). The static entry points keep their raw
signatures, so rule implementations are untouched.

Net -418 lines; TermLabelManager shrinks from ~1990 to 1629 lines.

Verified: full sequent+label equivalence over the 17-proof corpus
(byte-identical), logic tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@unp1 unp1 added 🛠 Maintenance Code quality and related things w/o functional changes RFC "Request for comments" is the appeal for making and expressing your opinion on a topic. labels Jul 3, 2026
@unp1 unp1 added this to the v3.1.0 milestone Jul 3, 2026
@unp1 unp1 self-assigned this Jul 3, 2026
unp1 added a commit to KeYProject/key-docs that referenced this pull request Jul 3, 2026
Flag both term-label pages as documenting an as-yet-unmerged pull request whose
details may change during review.
In the unchanged branch of the visitor the term handed to the label manager is
structurally identical to the visited term (same operator, subterms, bound
variables and labels). It was nevertheless rebuilt via
TermFactory.createTerm for every visited node, only to be discarded when the
node turned out to keep its labels. Pass the visited term directly instead,
via a new instantiateLabels(tacletTerm, newTerm) overload; the reconstructing
overload stays for the changed branch. Removes one term construction (and,
since labeled terms are interned, one StrictTermKey allocation and cache
lookup) per unchanged node.

Behaviour-preserving: the 17-proof label-equivalence dump stays byte-identical.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@unp1 unp1 requested a review from flo2702 July 3, 2026 13:12
* This is the middle one of the three label comparison modes: plain
* {@link de.uka.ilkd.key.logic.JTerm#equals(Object)} ignores <em>all</em> term labels, this
* property ignores only the non-proof-relevant (cosmetic) ones, and
* {@link de.uka.ilkd.key.logic.JTerm#equalsIncludingLabels(Object)} respects all labels. Use this

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's also a TermLabelsProperty which is only used in test code and seems to be redundant with JTerm.equalsIncludingLabels. Should that be removed?

* @return {@code true} iff {@code o} is a term syntactically equal to this one with equal
* label sets on all subterms
*/
boolean equalsIncludingLabels(Object o);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably, the documentation here should reference IrrelevantTermLabelProperty, so people don't get confused and use one when they really want the other.

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

Labels

🛠 Maintenance Code quality and related things w/o functional changes RFC "Request for comments" is the appeal for making and expressing your opinion on a topic.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants