Skip to content

JMLtk takes over the Parsing and Everything - #3997

Draft
wadoon wants to merge 51 commits into
mainfrom
weigl/jmltk-jml
Draft

wadoon wants to merge 51 commits into
mainfrom
weigl/jmltk-jml

Conversation

@wadoon

@wadoon wadoon commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Intended change

Rewrite how KeY handles JML specifications: remove KeY's own hand-written ANTLR JML front-end and the entire TextualJMLConstruct pre-parsing machinery (plus SpecificationRepository), and consume JMLtk's parsed JML AST (ast.jml: JmlContract, JmlClause, JmlClassLevelDeclaration, …) directly as the single source of truth. KeY keeps its specification semantics: the JMLSpecFactory translation (JML terms → JavaDL terms/speclang objects) remains, with JmlToTextual re-serializing each JMLtk clause/declaration against KeY's own JmlParser.g4 rules so the proven Translator/JmlTermFactory machinery is reused unchanged.

Status / progress

  • Create the design document (PLAN.md) and the construct-mapping/gap list (JML_MAPPING.md)
  • Phase 0 — JMLtk JML processing enabled in KeY's JavaParserFactory (setProcessJml(true), JML-keys policy)
  • Phase 1 — front-end swap (this PR), carrier-free design — JMLtk's typed JML AST is attached directly to the Java AST and stays there; there is no JmlSpec wrapper, no attachedJml fields, no data-key spec relocation:
    • JMLTransformer unwraps JmlMethodDeclaration/JmlFieldDeclaration wrappers back into the member list (Javaparser's symbol resolution ignores wrapped declarations) and remembers the wrapper on the inner node via the new KEY_JML_WRAPPER data key; it self-marks the JML statements (JmlExpressionStmt → KEY_ASSERT/KEY_ASSIGN, KeyMergePointStmt → KEY_MERGE_POINT, ghost variables → JML_GHOST).
    • JP2KeYConverter converts the Java declarations, skipping the pure class-level JML constructs (invariant, initially, axiom, represents, accessible — they stay on the JMLtk type), registers the KeY→JMLtk reverse mappings via KeYJPMapping.registerReverse (TypeDeclaration → JmlTypeDeclaration, each method/constructor → its contract carrier), translates JmlExpressionStmt → JmlAssert/SetStatement and KeyMergePointStmt → MergePointStatement, and createComments returns emptyList() (the parsed JML is not carried as comments anymore).
    • JMLSpecExtractor is rewritten to read the specs back from the JMLtk AST via KeYJPMapping.nodeFromKeY: method contracts and model-method definitions from the contract carrier, class-level specs from the JMLtk type members, loop invariants by scanning the JmlContracts on loop statements, block/loop/merge contracts from the attached contracts.
    • JMLSpecFactory gained direct-class overloads (createJMLOperationContracts(pm, JmlContract, ParserRuleContext modelMethodDefinition, boolean addInvariant), createJMLModelMethodContracts(...)), and the implicit-clause enrichment (purity assignables, \inv in requires/ensures/signals, non-null pre-/postconditions, default signals-only, synthetic MODEL_BEHAVIOR for model methods) moved there unchanged from the old extractor.
    • Removed: JmlSpec.java, the attachedJml carrier and getAssociatedSpecificationComments(), the KEY_SPEC_CASE/KEY_CLASS_SPEC relocation, PreParser/TextualTranslator and the TextualJML* intermediate layer.
  • Phase 2 (Option B, decided) — keep the JmlToTextual re-parse bridge as the semantics layer: JML grammars and Translator/JmlTermFactory/JmlIO/JmlFacade stay; only the dead pre-parsing code is deleted.
  • Phase 3 — stale-test cleanup + runtime parity; the speclang.* slice is the parity contract and is green.
  • Phase 4 — SpecificationRepository deleted, in nine slices (specs stay attached to the AST / owned by Services components; no global snapshot): slice 1 statement specs on JmlAssert/SetStatement (e222eb6b33), slice 2 limited observers (51c9a62dd7), slice 3 loop/block/merge specs on the statement nodes (63a536393b), slice 4 proof↔PO registry → ProofRegistry (b80363992a), slice 5 class invariants/axioms → ClassAxiomComputer (7495f9d167), slice 6 contract registry → ContractRegistry (ffc9fb9fd3), slice 7 initially clauses → JMLSpecFactory load path (3300c2e0c3), slice 8 addSpecs collapse + map(...) removal (4ca11a7e6d), slice 9 SpecificationRepository.java deleted (a962f66124).
  • Rebase onto origin/main (which contains the JMLtk Java-parser migration Migrate to JMLtk #3966, merged as 8bd51f0e59) and the final carrier-free front-end; jmltk stays pinned to 1.1.0-SNAPSHOT.

Latest updates (2026-10-30, pushed)

  • JMLTK_GAPS.md — new living status record of JML that worked before the takeover and its state under JMLtk (one entry per construct with re-verifiable fixtures, statuses measured at bf7048ca5d); supersedes the historical HANDOFF.md §5.2 triage in both directions (f830558081).
  • Spec-case names carried — the legacy name "..." clause cannot be parsed by JMLtk (it would silently drop the whole spec case). Named spec cases are now written in the JMLtk-native [behavior] <name>: prefix form (e.g. public normal_behavior CaseA: ...), which JMLtk parses into JmlContract.name; JmlToTextual.toSpecCase maps it to the spec-case name and JMLSpecFactory.generateName to the operation contract base name (consumer side already existed). JmlDocRemoval rejects the legacy string form loudly with a migration hint; class-level spec naming has no JMLtk counterpart (NameClauseClass stays a guarded failure). New positive fixture + test ContractLoadingTests.nameClauseCarriesContractName (a7a39edaa5).
  • Record pipeline goldens regenerated — the record goldens had been generated by the retired PreParser-based attachSpec and no longer matched the JMLtk front-end output (==/o == null re-serializations, [[JML modifiers]] marker placeholders, detached nullable comments, missing 17_AnnotationMover stage). expected/ was deliberately promoted from a fresh actual/ run (102 → 108 files); KeyJavaPipelineTest is green 142/0, byte-exact against a fresh run (841b999eee).
  • Docs updated to the measured state — HANDOFF.md §5.2 correction block and JMLTK_GAPS.md §6 status flip to resolved; the four now-green triage rows (merge family, JmlScriptTest, Issue3452Test incl. the \seq mis-attribution, TestProofStarter) were re-verified at HEAD (a8f86ee351).

Latest updates (2026-11-02, pushed)

  • jmltk snapshot refreshed and re-verified — the PR is re-verified against the freshly published 1.1.0-SNAPSHOT (2026-11-02; jmlparser-core, jmlparser-core-serialization, jmlparser-symbol-solver-core resolve from the Sonatype snapshots repository). :key.core compileJava/compileTestJava and the de.uka.ilkd.key.speclang.njml.* slice re-run on the rebased tip.
  • JMLtk-absent clauses locked with fixtures — new JMLParserExceptionTest cases in exceptional/:
    • NowarnDebug (class level) and NowarnDebugInMethod (method level) pin the level-dependent behavior of the //@ nowarn; / //@ debug; pragma-statements: class level raises a loud ProofInputException ("Encountered unexpected token"); method level falls back to KeY's own grammar and the leftover is dropped (JMLTK_GAPS.md §13).
    • MonitorsFor / ReadableIf / WritableIf / InGroup / MapsInto lock the JMLTransformer DATAGROUP_FAMILY guard per keyword: loud IllegalArgumentException, no spurious fields (JMLTK_GAPS.md §14 → resolved).
    • JMLTK_GAPS.md §13/§14 + status table, JML_MAPPING.md gaps 4–5 and PLAN.md item 5 updated to the measured state.
  • JMLTK_GAPS.md re-verified at the rebased tip — the full Appendix A suite (re)ran at 8fe1be5b6c (192 tests, 12 failed) and every recorded count confirms: merge family 12/12 (+ PredicateAbstractionLatticeTests 3/3), JmlScriptTest 7/7, Issue3452Test 2/2, ScDeterminismTest 4 load + 3 proof failures, RealProofMtEquivalenceTest 1 (sum0), KeyJavaPipelineTest 142/0 (108 golden files / 18 stages, incl. 17_AnnotationMover), LoopScopeInvRuleTests 1, 2 replayer failures, ParallelProverDeterminismTest 1 ("flags have to have the same size"). Rebased-hash references remapped (048f75d7df→bf7048ca5d, edbed7177c→251f945131, 10f4bfcb9a→841b999eee); fixture paths corrected (arrayMax/polishFlagSort live in standard_key/java_dl/, not heap/). §7 spec_*_math flipped OPEN → RESOLVED: the R4 modifier→SpecMathMode mapping is live at the tip with SpecMathModeTest (2) and ContractLoadingTests (6, incl. specMathJavaMathTest/specMathBigintMathTest) green. Pushed as f127079310, ed348636cb.
  • Note (pre-existing, not introduced by this refresh): two JMLParserExceptionTest cases (MethodInMethod, ModelFieldinMethod — a model declaration inside a method body) fail identically under the previous and the refreshed snapshot (A/B-verified byte-identical JMLtk parse). This is a KeY-side leftover-JML fallback gap, tracked separately from the recorded JMLTK_GAPS.md baseline.

Latest updates (2026-11-08, pushed)

  • CI red triage completed — every locally runnable red job is fixed or accounted for (record: TODO_CI.md). The red jobs of the 2026-11-02 CI run (key.core 14, keyext.slicing 5, keyext.caching 2) are now:
    • keyext.slicing: 5 red → fixed, 17/17 green. All five were stored-proof replay failures: the JMLtk front-end produced a KeY program/spec that drifted from the pre-takeover front-end, so proofs recorded with the old front-end no longer matched at replay. Four root causes, two commits:
      • 6a07c20e63 — the loopScopeInvDia family binds a fresh structurally-equal LoopStatement (position UNDEFINED) via \storeStmtIn whose node-attached invariant is null → structural reverse lookup LoopStatement.getLoopInvariant(program) (fixes the EndToEndTests slicing replays and LoopScopeInvRuleTests).
      • 233ec1d9b0 — (a) JMLtk attaches model-method contracts to the inner MethodDeclaration, not the JmlMethodDeclaration wrapper; the extractor only read the wrapper → trivial true contract and missing Contract_axiom_* taclet → now collected from both; (b) JML model-method bodies were built as executable Java bodies (\old → "not a known DL function name") → signature-only conversion when a JML wrapper is present (mirrors TextualJMLMethodDecl.getParsableDeclaration); (c) JMLtk kept //@ ghost … = null initializers on unwrapped fields, inlined into Throwable.$init → stripped in JMLTransformer (the old front-end materialized JML fields bare).
    • keyext.caching: 2 red → 1 fixed, 1 accounted. ProofCachingSingleRunTest is fixed by the loop-invariant fix (BinarySearch_search.key proves a loop_invariant+decreases loop); TestReferenceSearcher loads relax.proof — the same recorded OPEN gap as the two key.core replayers (JMLTK_GAPS.md §10; the replay succeeds but leaves the Null Reference case open whose postcondition frame derives from assignable \strictly_nothing, cf. gap <deleted> #15).
    • key.core: 14 → 11 red, all recorded, no new. The loop-invariant fix additionally resolved 3 recorded reds (LoopScopeInvRuleTests, arrayMax.key, polishFlagSort.key — all loop proofs; arrayMax/polishFlagSort were mis-attributed to \sum). KeyJavaPipelineTest 142/0 — the program-shape fixes do not change the pipeline goldens.
    • Integration jobs (testMt2w/testMt4w/testMtStress, testRunAll*Proofs) — triage complete (see the next bullet): testRunAllWdProofs is now green (was red on 2026-11-02); testMt2w/testMt4w = the recorded §4/§5 gaps surfacing through the curated small-MT collections (no new MT prover failure); testMtStress = the quicksort load gap (new §18).
  • jmltk bumped 1.1.0-SNAPSHOT → 1.2.1-SNAPSHOT (PR-author request). 1.2.1 parses the comment-form merge point (//@ merge_point …) into a new typed JmlMergePointStmt (ast.jml.stmt, new in 1.2.1) instead of leaving it an unparsed leftover comment; with no converter handler the default visitor aborts on the node's jmlTags SimpleName (A/B-verified: 1.1.0 green / 1.2.1 red, MergeRuleTests 9× "Load failed"). Handled natively in 53dcefd914: JP2KeYConverter.visit(JmlMergePointStmt) → MergePointStatement with a temporary boolean location variable (like the fallback form), plus a JMLSpecFactory.createJMLMergeContracts overload that re-serializes the node's merge_proc/merge_params metadata to the textual merge_point statement and re-parses it with KeY's JML grammar — so parametric merge works natively, no leftover-comment fallback. MergeRuleTests 12/12 + PredicateAbstractionLatticeTests 3/3 under 1.2.1; speclang.* slice re-verified (299 tests, 2 failed = the two known JMLParserExceptionTest cases, A/B-identical under 1.1.0/1.2.1, 10 skipped — no new failures).
  • JMLTK_GAPS.md re-measured at 53dcefd914 (4b62b1d6fa): key.core red count 14 → 11; §5 corrected (arrayMax/polishFlagSort are loop-invariant regressions, not \sum fidelity); §9 marked RESOLVED; §10 extended with TestReferenceSearcher and the node-40 root cause; §1 records the typed merge-point handling; new §17 documents the cosmetic [no file] origin term label (proof-irrelevant — OriginTermLabel.isProofRelevant() == false — causes no red job; documented, not fixed).
  • testRunAllFunProofs / testRunAllInfProofs per-proof triage complete (9ad2c13bd1, 854e6171f2). The testRunAll* jobs are CI-only, so the triage was done with local corpus runs at the branch tip (CI logs carry no failure messages). origin/main's Tests workflow is fully green — every Fun/Inf red is PR-induced by the front-end swap (they were invisible because the integration jobs were never part of the local baseline). The Fun set shrank 33 → 22 between the 1.1.0 (2026-11-02) and 1.2.1 (2026-11-08) CI runs — the 11 removed groups were fixed by the program/spec-shape commits. Classification (recorded in JMLTK_GAPS.md / TODO_CI.md):
    • §4 comma-represents → load failure (Fun list, javaCard, funOfIF, simple_info_flow, simpleTests; Inf NewObjects) — JMLtk hits a hard ANTLR problem on the locset comma.
    • §18 ghost locals → load failure (Fun quicksort) — JMLtk mis-reads //@ ghost \seq x = … as a class-level field.
    • §17 upgraded from cosmetic to a save→reload blocker (Fun other, list_seq) — a proof saved under the JMLtk front-end carries requires @ [no file] origin strings that OriginTermLabelFactory.parseOrigin rejects (renderer/parser asymmetry), so reload-enabled groups fail; fix options recorded.
    • §19 ensures_redundantly → load failure (Fun SmansEtAl) — JMLtk accepts the standard-JML clause but KeY's JmlParser.g4 has no production, so JmlToTextual throws "Unsupported clause kind".
    • §20 assignable_free → load failure (Fun javadl) — the converter runs the symbol resolver on the clause keyword → UnsolvedSymbolException.
    • §21 committed perm.proof replay drift (Fun reload_examples) — the tracked proof loads but its recorded replay now has 6 errors (stored rule position no longer fits, One Step Simplification not reconstructable) — a stale recording against the new front-end, same family as relax.proof.
    • proofs that no longer close — spec/sequent-shape drift (Fun newBook, comprehensions, blockContracts, list_ghost, observer, example-algos, VACID0, VSTTE10, FM2024Tutorial, WeideEtAl, redux; Inf ToyVoting{,_fullmacro}, ConditionalConfidential, SumExample*, ToyBanking{,_fullmacro}, SimpleEvoting{,_fullmacro}) — need per-proof debugging; not a single root cause.
    • The jmltk 1.2.1 bump added no new red groups (the 33→22 change is only the shape fixes). Inf SimpleEvoting_fullmacro is red in CI only (green locally — flaky, watching).

Known degradation (accepted)

  • merge_point is now covered for both forms (since jmltk 1.2.1, 53dcefd914): the Java statement merge_point(<expr>); (KeyMergePointStmt) is re-parsed as KeY's merge_point_statement, and the comment form (//@ merge_point, merge_proc, merge_params) is parsed by JMLtk into a typed JmlMergePointStmt and converted natively — including parametric merge (merge_proc "MergeByPredicateAbstraction", merge_params {...} as used by testcase/merge/*). The KEY_JML_FALLBACK leftover-comment re-parse is no longer needed for merge points (it remains for the other KeY-specific constructs, e.g. proof scripts, nowarn/debug pragmas).
  • The recorded baseline is tracked live in JMLTK_GAPS.md (measured at 53dcefd914, supersedes HANDOFF.md §5.2). Of the historical 34 recorded failures, the merge family, JmlScriptTest, Issue3452Test (KeY-side \seq name resolution, not a JMLtk gap) and TestProofStarter are green at HEAD (re-verified 2161/0); the record-goldens regression is fixed by the regeneration. Re-measured at 53dcefd914 (jmltk 1.2.1), the remaining recorded OPEN gaps are 9 failures: comma-separated store-ref represents (4 ScDeterminismTest load failures), \sum-proof fidelity (sum1.key/sum0.key), scripted-proof replay of relax.proof (3 tests: TestCopyingReplayer, TestEqualsModProofIrrelevancy, keyext.caching TestReferenceSearcher), and the pre-existing block-contract combination bug (ParallelProverDeterminismTest); labeled break/continue (LoopScopeInvRuleTests) and arrayMax/polishFlagSort are resolved by 6a07c20e63. In addition, the two known JMLParserExceptionTest cases (not part of the recorded baseline) remain; further open entries in JMLTK_GAPS.md §12–§21 (absent grammar tokens, \strictly_nothing, [no file] origin labels, quicksort ghost locals, ensures_redundantly, assignable_free, perm.proof replay drift) are not part of the recorded full-suite baseline. The testRunAll* corpus reds (22 Fun / 10–11 Inf groups, all PR-induced — see the 2026-11-08 bullet above) are likewise not part of the recorded baseline; they are the corpus-wide surface of the same gap families and need per-proof triage before this PR can claim corpus parity.

Remediation: no silent spec weakening

Follow-up work on the shipped front-end (242e6eb44d) that closes every path where a JML construct JMLtk accepts could be silently dropped, weakened, or mis-translated on its way through KeY's own JML grammar and term translation.

  • R0/R1 — loud guards replace catch-and-skip. JmlToTextual hard-fails on untranslatable clauses (KNOWN_GAPS starts empty; skipping requires triaging the loss as acceptable), and the new JmlSideEffectChecker structurally rejects assignment operators, ++/--, and object/array creation in JML specification expressions (set/ghost statements exempt; never descends into Java bodies). Wired into all contract/loop/class-level clause translations and the assert/assume branch of JP2KeYConverter.
  • R6 — JMLtk snapshot pin documented. Release 1.0.0 lacks the ast.jml.proof package (proof scripts assert e \by { ... }), so the 1.1.0-SNAPSHOT + snapshots repository stay; build.gradle comment tracks jmlparser-core 1.0.0 lacks ast.jml.proof (assert \by {...}) support shipped in 1.1.0-SNAPSHOT jmltoolkit/jmltk#82 for the pin-on-release path.
  • R5 — parallel-prover merge-contract safety. Synchronized contract accessors on StatementBlock/LoopStatement/MergePointStatement and a new atomic setMergeContracts; the former racy removeMergeContracts() + addMergeContract() sequences in ProgVarReplaceVisitor and IntroAtPreDefsOp are gone (concurrent rule application never observes an empty contract set).
  • R2.1 — assertion proof scripts retained. JP2KeYConverter re-parses the JMLtk JmlProofStmt list against KeY's assertionProof grammar into KeyAst.JMLProofScript (assert and assume); a script JMLtk accepted but KeY cannot re-parse fails loudly instead of being dropped. Locked by AssertionProofScriptTest.
  • R2.2 — quantifier round-trip locked. SumClause/BsumClause fixtures assert \sum and \bsum (canonical (\bsum T v; from; to; summand) range form) survive the JMLtk → KeY re-parse verbatim; \bsum keeps its loud deprecation warning.
  • R4 — spec_*_math pragmas restored. Class-level /*@ spec_java_math @*/ / /*@ spec_bigint_math @*/ in the specMath fixtures are parsed by JMLtk as real modifiers and flow through ModifierKind.JML_SPEC_* into JMLInfoExtractor; specMathJavaMathTest/specMathBigintMathTest are re-enabled and SpecMathModeTest locks class, method and inline model-method modes.
  • Additionally: the class-level "field-like" clause handling in JMLTransformer rejects the datagroup family loudly (datagroup, in_group, maps_into, monitors_for, readable_if, writable_if, mapped_by, repr) instead of installing spurious fields, keeps captures/duration/working_space as accepted drops, signals clauses omit the synthesized \exception binder, and the information-flow serializer was rewritten to JMLtk's shifted getters with loop_determines/loop_separates wired to KeY's loop grammar. New negative/positive fixtures: WrongEquals (now a loud JmlSideEffectException), IncrementInContract, ObjectCreationInContract, NoSideEffects, Datagroup, DeprecatedDrops, NameClause (positive, [behavior] <name>: form), NameClauseClass (class-level legacy name "..." guard).

Test status

  • Caveat at the refreshed tip (8fe1be5b6c): two JMLParserExceptionTest cases (MethodInMethod, ModelFieldinMethod — a model declaration in a method body) fail; pre-existing and identical under the previous and the refreshed snapshot (KeY-side leftover-JML fallback gap, see the 2026-11-02 section).
  • de.uka.ilkd.key.speclang.* slice: 299 tests, 2 failed, 10 skipped (measured at 53dcefd914 under jmltk 1.2.1-SNAPSHOT; the 2 failures are the known JMLParserExceptionTest pair, no new failures; before the 2026-11-02 fixture additions: 292/0/10 after the remediation commit 242e6eb44d, the two previously-@Disabled specMath* tests green again).
  • KeyJavaPipelineTest (record code-transformation pipeline): 142 tests, 0 failures after the deliberate golden regeneration (102 → 108 expected files, incl. the new 17_AnnotationMover stage; 86 previously-differing goldens).
  • The four triage rows now green were re-verified at HEAD: MergeRuleTests 12, MtMergePointTest 1, TestTacletEquality 2137, JmlScriptTest 7, Issue3452Test 2, TestProofStarter 2 — 2161 tests, 0 failures.
  • Whole build compileJava + compileTestJava (incl. key.ui and the keyext modules), spotlessCheck and the Checker-Framework nullness build (-DENABLE_NULLNESS=true compileTestJava): green.

Note on the diff

The branch was rebased onto origin/main, which already contains the JMLtk Java-parser migration (#3966, merged as 8bd51f0e59). The PR diff therefore contains only the JML front-end work: the typed-JML-AST-based spec extraction plus the jmltk pin bump 1.0.0 → 1.2.1-SNAPSHOT, the silent-spec-weakening remediation 242e6eb44d (loud guards, proof-script/quantifier/spec-math parity, parallel-prover contract safety), and the recent pushed commits f830558081 (JMLTK_GAPS), a7a39edaa5 (spec-case names), 841b999eee (record-golden regeneration), a8f86ee351 (measured-state docs update), 8fe1be5b6c (nowarn/debug + datagroup-family fixtures), 6a07c20e63 (structural loop-invariant lookup), 233ec1d9b0 (pre-takeover program/spec shape for model methods and JML fields), 53dcefd914 (JMLtk 1.2.1 typed merge points + jmltk bump), 4b62b1d6fa (CI triage record TODO_CI.md + JMLTK_GAPS re-measurement).

Type of pull request

  • Refactoring (behaviour should not change or only minimally change)
  • There are changes to the (Java) code

Ensuring quality

  • I made sure that introduced/changed code is well documented (javadoc and inline comments).
  • I have tested the feature as follows: the speclang test-suite in key.core is the parity contract for the implementation and is green on the rebased branch (299/2/10 at 53dcefd914 under jmltk 1.2.1-SNAPSHOT, the 2 failures being the known JMLParserExceptionTest pair, no new failures); KeyJavaPipelineTest is green (142/0) after the deliberate golden regeneration; the previously-red keyext.slicing module is green (17/17) and keyext.caching has a single accounted failure (the recorded relax.proof gap); whole-build compileJava/compileTestJava, spotlessCheck and the Checker nullness build are green at the branch tip; the remaining recorded gaps are tracked live in JMLTK_GAPS.md (supersedes HANDOFF.md §5.2) with zero new regressions.

@wadoon
wadoon marked this pull request as draft October 24, 2026 02:34
@wadoon
wadoon changed the base branch from main to weigl/jmltk October 24, 2026 02:35
@wadoon
wadoon added this pull request to stack #3998 October 24, 2026 02:41
@wadoon wadoon changed the title Design plan: rewrite JML specification handling on top of JMLtk JMLtk front-end: consume JMLtk's parsed JML AST (Phases 0+1) Oct 24, 2026
@wadoon

wadoon commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

Phase 4 progress — slices 1–3 merged into this branch (weigl/jmltk-jml)

Phase 4 deletes SpecificationRepository (a second, redundant copy of the
parsed/translated JML) slice by slice; each slice is committed, pushed, and
verified against the speclang slice before moving on. Current branch HEAD:
248b69f4f4.

Slice Content Commit
1 JmlStatementSpec attached to JmlAssert/SetStatement nodes; repo statementMap deleted 7f369e5c29
2 limited observers (limitObs/unlimitObs/taclets) → de.uka.ilkd.key.util.LimitedObservers (stateless, thread-safe) 806c2b1ba6
triage drop class-level code_*_math/spec_*_math pragmas from intSemantics fixtures (jmltk parity); IntSemanticsTest 6/6 14a26a43d0
3 loop invariants, block/loop/merge contracts attached to LoopStatement/StatementBlock/MergePointStatement nodes; repo loopInvs/blockContracts/loopContracts/loopContractsOnLoops/mergeContracts maps + helpers deleted (~390 lines) ae716ed74a
docs progress recorded in HANDOFF.md/PLAN.md 248b69f4f4

Slice 3 details: specs attach to the AST nodes they describe; visible
contract lookup reads the nodes (Abstract{Block,Loop}ContractRule +
AbstractAuxiliaryContractRule.get{Block,Loop}Contracts keep the exact
modality/transaction filtering); SLEnvInput attaches at load time;
ProgVarReplaceVisitor/IntroAtPreDefsOp rewrite node state (merge
contracts keep their remove-then-re-add semantics); all rules, conditions,
loop-elimination meta-constructs, break/continue replacers, functional POs,
key.ui completion and key.core.infflow read/write node state.

Verification at the slice-3 HEAD (ae716ed74a):

  • :key.core:compileJava, :key.ui:compileJava, key.core.infflow: green
  • speclang.* slice: 285 tests / 0 fail / 12 skip (matches baseline)
  • IntSemanticsTest: 6 / 0 / 0
  • Full suite: 1638 / 34 fail / 72 skip — the 34 failures are the recorded
    pre-existing jmltk gaps from HANDOFF.md §5.2 (A/B-verified identical to
    pre-Phase-4), i.e. zero slice regressions.

Next: slice 4 = proof↔PO registry (proofs, registerProof/removeProof,
getPO*) → the proof layer, then class invariants/axioms
(ClassAxiomComputer + identity memo), the contract registry, initially
clauses, and finally deletion of SpecificationRepository.java.

@wadoon

wadoon commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

Phase 4 progress — slices 1–4 merged into this branch (weigl/jmltk-jml)

Phase 4 deletes SpecificationRepository (a second, redundant copy of the
parsed/translated JML) slice by slice; each slice is committed, pushed, and
verified against the speclang slice before moving on. Current branch HEAD:
7534a73489.

Slice Content Commit
1 JmlStatementSpec attached to JmlAssert/SetStatement nodes; repo statementMap deleted 7f369e5c29
2 limited observers (limitObs/unlimitObs/taclets) → de.uka.ilkd.key.util.LimitedObservers (stateless, thread-safe) 806c2b1ba6
triage drop class-level code_*_math/spec_*_math pragmas from intSemantics fixtures (jmltk parity); IntSemanticsTest 6/6 14a26a43d0
3 loop invariants, block/loop/merge contracts attached to LoopStatement/StatementBlock/MergePointStatement nodes; repo loopInvs/blockContracts/loopContracts/loopContractsOnLoops/mergeContracts maps + helpers deleted (~390 lines) ae716ed74a
4 proof↔PO registry (proofs map, registerProof/removeProof/getProofs/getPO*/getProofOblInput/getContractPOForProof/getTargetOfProof/getAllProofs) → new de.uka.ilkd.key.proof.mgt.ProofRegistry owned by Services (repo proof block + proofs field deleted, ~160 lines) c81c034dd1
docs progress recorded in HANDOFF.md/PLAN.md 248b69f4f4, 7534a73489

Slice 4 details: the proof↔PO bookkeeping moves verbatim out of
SpecificationRepository into a new ProofRegistry class in the
proof-management layer. It is owned by Services (not ProofEnvironment)
because env-less loader paths (KeYFacade.loadProofFile,
AbstractProblemLoader) register/query proofs through the shared
initConfig.getServices() without creating an environment, and
Services.copy() shares it like specRepos (side-proof envs, e.g. testgen,
keep the same registry as their source proof). All ~56 call sites across
key.core, key.core.infflow, key.core.testgen, key.core.wd, key.ui,
keyext.proofmanagement and keyext.ui.testgen were rewired; the redundant
pre-environment registerProof in
DefaultUserInterfaceControl.createProofEnvironmentAndRegisterProof was
dropped (env.registerProof registers every list proof with the same PO →
identical bucket state). The contract-based lookups
(getProofs(Contract)/getProofs(kjt,target)) still delegate
splitContract/getOverridingTargets to the repository — those helpers move
along with the contract registry in a later slice (documented in javadoc).

Verification at the slice-4 HEAD (c81c034dd1, docs 7534a73489):

  • :key.core:compileJava (incl. key.core.infflow/testgen/wd compile deps),
    :key.ui:compileJava, keyext.* : green
  • speclang.* slice: 285 tests / 0 fail / 12 skip (matches baseline)
  • IntSemanticsTest: 6 / 0 / 0
  • Full suite: 1638 / 34 fail / 72 skip — the 34 failures are the recorded
    pre-existing jmltk gaps from HANDOFF.md §5.2 (A/B-verified identical to
    pre-Phase-4), i.e. zero slice regressions.

Next: class invariants/axioms (ClassAxiomComputer + per-KeYJavaType
identity memo for the invs/axioms maps and getClassAxioms incl. the
synthesized invariant-definition/QueryAxiom/model-method axioms), then the
contract registry (contracts/operationContracts/contractsByName/
contractTargets + splitContract/getOverridingTargets/
getInheritedContracts/prepareContract/contractCounters), initially
clauses (→ JMLSpecFactory at load), addSpecs collapse + map(...)
removal, and finally deletion of SpecificationRepository.java and the
Services/KeYEnvironment repository accessors.

@wadoon

wadoon commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

Phase 4 progress — slices 1–5 merged into this branch (weigl/jmltk-jml)

Phase 4 deletes SpecificationRepository (a second, redundant copy of the
parsed/translated JML) slice by slice; each slice is committed, pushed, and
verified against the speclang slice before moving on. Current branch HEAD:
1b0f1f94ee (docs pending).

Slice Content Commit
1 JmlStatementSpec attached to JmlAssert/SetStatement nodes; repo statementMap deleted 7f369e5c29
2 limited observers (limitObs/unlimitObs/taclets) → de.uka.ilkd.key.util.LimitedObservers (stateless, thread-safe) 806c2b1ba6
triage drop class-level code_*_math/spec_*_math pragmas from intSemantics fixtures (jmltk parity); IntSemanticsTest 6/6 14a26a43d0
3 loop invariants, block/loop/merge contracts attached to LoopStatement/StatementBlock/MergePointStatement nodes; repo loopInvs/blockContracts/loopContracts/loopContractsOnLoops/mergeContracts maps + helpers deleted (~390 lines) ae716ed74a
4 proof↔PO registry (proofs map, registerProof/removeProof/getProofs/getPO*/getProofOblInput/getContractPOForProof/getTargetOfProof/getAllProofs) → new de.uka.ilkd.key.proof.mgt.ProofRegistry owned by Services (repo proof block + proofs field deleted, ~160 lines) c81c034dd1
5 class invariants/axioms (invs/axioms/allClassAxiomsCache, getClassInvariants/addClassInvariant(s), getClassAxioms incl. synthesized invariant-definition/QueryAxiom/model-method axioms, getClassAxiomsForType, addClassAxiom(s)) → new de.uka.ilkd.key.speclang.ClassAxiomComputer owned by Services (repo class-axiom surface deleted, ~340 lines) 1b0f1f94ee
docs progress recorded in HANDOFF.md/PLAN.md 248b69f4f4 (slice 3)

Slice 5 details: the invariant/class-axiom machinery moves verbatim out of
SpecificationRepository into ClassAxiomComputer — a Services-owned,
copy-shared component (created via a new Profile.createClassAxiomComputer
hook) that owns invs/axioms/allClassAxiomsCache and the whole
getClassAxioms(kjt) computation (visible-and-own axioms, synthesized
inv/staticInv/inv_free/staticInv_free definition axioms, QueryAxioms,
RepresentsAxiom/ContractAxiom model-method axioms, enclosing-class
recursion) with the per-KeYJavaType identity memo intact (same instances
must be returned because ClassAxiom has no equals/hashCode). The WD
profile gets a ClassAxiomComputerWD subclass that keeps registering
ClassWellDefinedness checks when invariants are added (previously
SpecificationRepositoryWD.addClassInvariant override). The computer reads
operation contracts through a documented temporary bridge to the still-alive
repository (getModelMethodAxioms); that bridge disappears with the contract
registry. Repo map(...) now delegates its class-axiom maps to the computer.

Verification at the slice-5 HEAD (1b0f1f94ee):

  • :key.core:compileJava + key.core.wd/testgen/infflow, keyext.proofmanagement,
    :key.ui:compileJava (+ their test sources): green
  • speclang.* slice: 285 tests / 0 fail / 12 skip (matches baseline)
  • IntSemanticsTest: 6 / 0 / 0
  • Full suite: 1638 / 34 fail / 72 skip — the 34 failures are the recorded
    pre-existing jmltk gaps from HANDOFF.md §5.2 (A/B-verified identical to
    pre-Phase-4), i.e. zero slice regressions.

Next: the contract registry (contracts/operationContracts/
contractsByName/contractTargets + splitContract/getOverridingTargets/
getInheritedContracts/prepareContract/contractCounters → per-node
derived cache + JMLSpecFactory; unblocks the ProofRegistry contract-lookup
delegation), then initially clauses (→ JMLSpecFactory at load), addSpecs
collapse + map(...) removal, and finally deletion of
SpecificationRepository.java and the Services/KeYEnvironment repository
accessors.

@wadoon

wadoon commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

Phase 4 progress — slices 1–6 merged into this branch (weigl/jmltk-jml)

Phase 4 deletes SpecificationRepository (a second, redundant copy of the
parsed/translated JML) slice by slice; each slice is committed, pushed, and
verified against the speclang slice before moving on. Current branch HEAD:
005320b2ec (docs pending).

Slice Content Commit
1 JmlStatementSpec attached to JmlAssert/SetStatement nodes; repo statementMap deleted 7f369e5c29
2 limited observers (limitObs/unlimitObs/taclets) → de.uka.ilkd.key.util.LimitedObservers (stateless, thread-safe) 806c2b1ba6
triage drop class-level code_*_math/spec_*_math pragmas from intSemantics fixtures (jmltk parity); IntSemanticsTest 6/6 14a26a43d0
3 loop invariants, block/loop/merge contracts attached to LoopStatement/StatementBlock/MergePointStatement nodes; repo loopInvs/blockContracts/loopContracts/loopContractsOnLoops/mergeContracts maps + helpers deleted (~390 lines) ae716ed74a
4 proof↔PO registry (proofs map, registerProof/removeProof/getProofs/getPO*/getProofOblInput/getContractPOForProof/getTargetOfProof/getAllProofs) → new de.uka.ilkd.key.proof.mgt.ProofRegistry owned by Services (repo proof block + proofs field deleted, ~160 lines) c81c034dd1
5 class invariants/axioms (invs/axioms/allClassAxiomsCache, getClassInvariants/addClassInvariant(s), getClassAxioms incl. synthesized invariant-definition/QueryAxiom/model-method axioms, getClassAxiomsForType, addClassAxiom(s)) → new de.uka.ilkd.key.speclang.ClassAxiomComputer owned by Services (repo class-axiom surface deleted, ~340 lines) 1b0f1f94ee
6 contract registry: contracts/operationContracts/contractsByName/contractTargets, splitContract/combineOperationContracts, getOverridingTargets/getInheritedContracts, prepareContract/contractCounters + initially clauses → new de.uka.ilkd.key.speclang.ContractRegistry owned by Services (via Profile.createContractRegistry; WD hooks → ContractRegistryWD); CONTRACT_COMBINATION_MARKER → ContractFactory; TacletPOGenerator takes Services; repo shrinks to a thin dispatcher (+208/−993) 005320b2ec
docs progress recorded in HANDOFF.md/PLAN.md 248b69f4f4 (slice 3)

Slice 6 details: the contract machinery moves verbatim out of
SpecificationRepository into a new de.uka.ilkd.key.speclang.ContractRegistry
— a Services-owned, copy-shared component created via a new
Profile.createContractRegistry hook — that owns the per-(KeYJavaType, IObserverFunction) contract maps (contracts/operationContracts/
contractsByName/contractTargets), the contract id counters
(prepareContract/contractCounters), the initially clauses, and the
resolution helpers (getOverridingTargets/getInheritedContracts/
splitContract/combineOperationContracts/getContractByName/map),
unblocking the ProofRegistry contract-lookup delegation. The WD profile gets
a ContractRegistryWD subclass absorbing every SpecificationRepositoryWD
hook (wdChecks, register/unregister overrides, the WD contract filters,
getWdChecks/getAllWdChecks/getWdMethodChecks/getWdClassChecks). The
§ CONTRACT_COMBINATION_MARKER moves to ContractFactory;
TacletPOGenerator.generate now takes Services (its only impl,
WDTacletGenerator, casts the registry to ContractRegistryWD). The repo is
now a thin dispatcher (map/addSpecs/empty processJavaType).

Verification at the slice-6 HEAD (005320b2ec):

  • :key.core:compileJava + key.core.wd/testgen/infflow, keyext.proofmanagement,
    :key.ui:compileJava (+ their test sources): green
  • speclang.* slice: 285 tests / 0 fail / 12 skip (matches baseline)
  • IntSemanticsTest: 6 / 0 / 0
  • Full suite (key.core): 1640 / 34 fail / 74 skip — the 34 failures are
    the recorded pre-existing jmltk gaps from HANDOFF.md §5.2 (A/B-verified
    identical to pre-Phase-4), i.e. zero slice regressions.
  • Module suites at slice-6 HEAD: key.core.wd 1/0/0, key.core.testgen
    15/2/0, key.ui 44/1/0 green; the 3 InfFlowProfileLoadingTest failures
    are pre-existing (verified identical at Phase-3 and slice-5 HEADs).

Next: slice 7 — move the initially clauses (initiallyClauses/
addInitiallyClause(s)/createContractsFromInitiallyClauses) out of the
ContractRegistry into the JMLSpecFactory load path, then addSpecs
collapse + map(...) removal, and finally deletion of
SpecificationRepository.java and the Services/KeYEnvironment repository
accessors.

@wadoon

wadoon commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

Phase 4 progress — slices 1–7 merged into this branch (weigl/jmltk-jml)

Phase 4 deletes SpecificationRepository (a second, redundant copy of the
parsed/translated JML) slice by slice; each slice is committed, pushed, and
verified against the speclang slice before moving on. Current branch HEAD:
fd32802417 (docs pending).

Slice Content Commit
1 JmlStatementSpec attached to JmlAssert/SetStatement nodes; repo statementMap deleted 7f369e5c29
2 limited observers (limitObs/unlimitObs/taclets) → de.uka.ilkd.key.util.LimitedObservers (stateless, thread-safe) 806c2b1ba6
triage drop class-level code_*_math/spec_*_math pragmas from intSemantics fixtures (jmltk parity); IntSemanticsTest 6/6 14a26a43d0
3 loop invariants, block/loop/merge contracts attached to LoopStatement/StatementBlock/MergePointStatement nodes; repo loopInvs/blockContracts/loopContracts/loopContractsOnLoops/mergeContracts maps + helpers deleted (~390 lines) ae716ed74a
4 proof↔PO registry (proofs map, registerProof/removeProof/getProofs/getPO*/getProofOblInput/getContractPOForProof/getTargetOfProof/getAllProofs) → new de.uka.ilkd.key.proof.mgt.ProofRegistry owned by Services (repo proof block + proofs field deleted, ~160 lines) c81c034dd1
5 class invariants/axioms (invs/axioms/allClassAxiomsCache, getClassInvariants/addClassInvariant(s), getClassAxioms incl. synthesized invariant-definition/QueryAxiom/model-method axioms, getClassAxiomsForType, addClassAxiom(s)) → new de.uka.ilkd.key.speclang.ClassAxiomComputer owned by Services (repo class-axiom surface deleted, ~340 lines) 1b0f1f94ee
6 contract registry: contracts/operationContracts/contractsByName/contractTargets, splitContract/combineOperationContracts, getOverridingTargets/getInheritedContracts, prepareContract/contractCounters + initially clauses → new de.uka.ilkd.key.speclang.ContractRegistry owned by Services (via Profile.createContractRegistry; WD hooks → ContractRegistryWD); CONTRACT_COMBINATION_MARKER → ContractFactory; TacletPOGenerator takes Services; repo shrinks to a thin dispatcher (+208/−993) 005320b2ec
7 initially clauses (initiallyClauses map, addInitiallyClause(s), createContractsFromInitiallyClauses) → JMLSpecFactory at load: SLEnvInput.createSpecs partitions InitiallyClause out of the class specs into its per-load factory and calls the constructor-postcondition derivation at the end of loading (same ordering); registry loses the field + 4 methods, map(...) no longer maps them, unregisterContract becomes public (WD override bumped); repo addSpecs stops dispatching InitiallyClause fd32802417
docs progress recorded in HANDOFF.md/PLAN.md 248b69f4f4 (slice 3)

Slice 7 details: the initially-clause machinery moves out of
ContractRegistry into JMLSpecFactory, where the constructor-postcondition
derivation belongs (its ContractFactory.cf was already translating
InitiallyClauses via initiallyClauseToContract). The factory now owns the
per-KeYJavaType initiallyClauses map + addInitiallyClause(s) (collection
during loading) and createContractsFromInitiallyClauses() (+ the private
derivation helper), called once at the end of SLEnvInput.createSpecs —
SLEnvInput partitions InitiallyClause out of the class specs and feeds them
to the per-load factory it already owned, so the derivation ordering is
unchanged. The derived constructor contracts are registered through
services.getContractRegistry() via addContractNoInheritance/
unregisterContract/cf.addPost/cf.func; unregisterContract becomes
public so the factory can replace constructor contracts (the WD subclass
override is bumped to public accordingly), and the WD
registerContract/unregisterContract hooks keep creating the WD checks.
ContractRegistry drops the field + 4 methods and no longer maps initially
clauses; repo addSpecs is now Contract/ClassInvariant/ClassAxiom/
loop/block/merge only.

Verification at the slice-7 HEAD (fd32802417):

  • :key.core:compileJava + key.core.wd/testgen/infflow, keyext.proofmanagement,
    :key.ui:compileJava (+ their test sources): green
  • speclang.* slice + IntSemanticsTest: 285 / 0 / 12 (matches baseline)
  • Module suites (run solo — a first combined run OOM'd, exit 137, under
    concurrent load): key.core.wd 1/0/0, key.core.testgen 15/2/0, key.ui
    44/1/0 green; key.core.infflow 4/3 = only the 3 pre-existing
    InfFlowProfileLoadingTest failures (verified identical at Phase-3 and
    slice-5 HEADs).

Next: slice 8 — collapse addSpecs (the dispatch moves into the
SLEnvInput/JMLSpecFactory output handling) and remove map(...) from the
repo/registry/computer surface, then the final deletion of
SpecificationRepository.java and the Services/KeYEnvironment repository
accessors.

@wadoon

wadoon commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

Phase 4 complete — SpecificationRepository is deleted (slices 1–9 merged
into this branch, weigl/jmltk-jml)

Phase 4 deleted SpecificationRepository (a second, redundant copy of the
parsed/translated JML) slice by slice; each slice was committed, pushed, and
verified against the speclang slice before moving on. The final slice
(e50d64b699) removes the last remaining state — the per-type processJavaType
hook, which moves into the Services-owned ContractRegistryWD — and deletes the
repository, the Services/KeYEnvironment accessors, the
Profile.createSpecificationRepository hook, the WdProfile override and
SpecificationRepositoryWD. All specifications now live on the AST nodes or in
the Services-owned ContractRegistry/ClassAxiomComputer/ProofRegistry/
JMLSpecFactory.

Slice Content Commit
1 JmlStatementSpec attached to JmlAssert/SetStatement nodes; repo statementMap deleted 7f369e5c29
2 limited observers (limitObs/unlimitObs/taclets) → de.uka.ilkd.key.util.LimitedObservers (stateless, thread-safe) 806c2b1ba6
triage drop class-level code_*_math/spec_*_math pragmas from intSemantics fixtures (jmltk parity); IntSemanticsTest 6/6 14a26a43d0
3 loop invariants, block/loop/merge contracts attached to LoopStatement/StatementBlock/MergePointStatement nodes; repo loopInvs/blockContracts/loopContracts/loopContractsOnLoops/mergeContracts maps + helpers deleted (~390 lines) ae716ed74a
4 proof↔PO registry (proofs map, registerProof/removeProof/getProofs/getPO*/getProofOblInput/getContractPOForProof/getTargetOfProof/getAllProofs) → new de.uka.ilkd.key.proof.mgt.ProofRegistry owned by Services (repo proof block + proofs field deleted, ~160 lines) c81c034dd1
5 class invariants/axioms (invs/axioms/allClassAxiomsCache, getClassInvariants/addClassInvariant(s), getClassAxioms incl. synthesized invariant-definition/QueryAxiom/model-method axioms, getClassAxiomsForType, addClassAxiom(s)) → new de.uka.ilkd.key.speclang.ClassAxiomComputer owned by Services (repo class-axiom surface deleted, ~340 lines) 1b0f1f94ee
6 contract registry: contracts/operationContracts/contractsByName/contractTargets, splitContract/combineOperationContracts, getOverridingTargets/getInheritedContracts, prepareContract/contractCounters + initially clauses → new de.uka.ilkd.key.speclang.ContractRegistry owned by Services (via Profile.createContractRegistry; WD hooks → ContractRegistryWD); CONTRACT_COMBINATION_MARKER → ContractFactory; TacletPOGenerator takes Services; repo shrinks to a thin dispatcher (+208/−993) 005320b2ec
7 initially clauses (initiallyClauses map, addInitiallyClause(s), createContractsFromInitiallyClauses) → JMLSpecFactory at load: SLEnvInput.createSpecs partitions InitiallyClause out of the class specs into its per-load factory and calls the constructor-postcondition derivation at the end of loading (same ordering); registry loses the field + 4 methods, map(...) no longer maps them, unregisterContract becomes public (WD override bumped); repo addSpecs stops dispatching InitiallyClause fd32802417
8 addSpecs collapse (dispatch into a private SLEnvInput.registerSpecs helper: Contract → registry, ClassInvariant/ClassAxiom → computer, loop/block/merge → AST nodes) + map(...) removal from the repo/registry/computer surface (zero callers; thread-safety comment updated — nothing is mutated during proving anymore) 2ae5b95be0
9 SpecificationRepository.java deleted: processJavaType → ContractRegistry (empty default) + ContractRegistryWD (former SpecificationRepositoryWD body); Services/KeYEnvironment accessors, Profile.createSpecificationRepository, WdProfile override and SpecificationRepositoryWD.java deleted e50d64b699

Slice 9 details: the last remaining piece of the repository was the
per-type processJavaType hook, which profiles (WD) used to post-process a
type's specifications. It moves to the profile-created Services component the
repository used to front — ContractRegistry gets the empty default and
ContractRegistryWD the WD override (the RepresentsAxiom → WD-check
processing from SpecificationRepositoryWD, with the internal cast to
ContractRegistryWD collapsing to this). SLEnvInput.createSpecs now calls
contractRegistry.processJavaType(kjt); the specRepos variable, the
Services.specRepos field + getSpecificationRepository() + copy paths, the
KeYEnvironment.getSpecificationRepository(), Profile.createSpecification- Repository and the WdProfile override are deleted, as are the two repository
classes. Javadoc mentions of the class were downgraded to plain text so no
dangling @link remains.

Verification at the Phase-4-completion HEAD (e50d64b699):

  • :key.core:compileJava + key.core.wd/testgen/infflow, keyext.proofmanagement,
    :key.ui:compileJava (+ their test sources): green
  • speclang.* slice + IntSemanticsTest: 285 / 0 / 12 (matches baseline)
  • Module suites (run solo — combined runs OOM, exit 137, under concurrent
    load): key.core.wd 1/0/0, key.core.testgen 15/2/0, key.ui 44/1/0
    green; key.core.infflow 4/3 = only the 3 pre-existing
    InfFlowProfileLoadingTest failures (verified identical at Phase-3 and
    slice-5 HEADs)
  • Full suite: 1640 / 34 fail / 74 skip — the 34 failures are the recorded
    pre-existing jmltk gaps from HANDOFF.md §5.2 (A/B-verified identical to
    pre-Phase-4), i.e. zero slice regressions.

Phase 4 is done; nothing remains in the plan table but ✅ rows. Possible
follow-ups (not required): move the prepareContract/contractCounters id
counters from ContractRegistry into JMLSpecFactory/ContractFactory at
derivation time (see PLAN).

@wadoon wadoon changed the title JMLtk front-end: consume JMLtk's parsed JML AST (Phases 0+1) JMLtk takes over the Parsing and Everything Oct 24, 2026
@wadoon

wadoon commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

CI triage after the Phase-4 slices (7–9) + spotless fix

Fixed: formatting job (spotlessJavaCheck had been failing for the whole
branch). spotlessApply over the 25 affected files — all of them within this
PR's diff (import ordering in key.ui/keyext.proofmanagement, javadoc/line
reflow and statement wrapping in key.core/key.core.wd) — commit e17111ed30.
CI CodeQuality (formatting + checkerFramework) is green again.

Everything else that is red was already red before Phase 4 slices 7–9 —
verified by comparing the failing job sets against the CI runs at commit
d1321bec5d (right after slice 7) and the slice-3/-4-era commits
ae716ed74a/c81c034dd1: identical set, so slices 7–9 introduced zero new
failures. Base branch weigl/jmltk CI is green; these all trace to the Phase
0–3 front-end swap.

Job Failures Status
unit-tests key.core (ubuntu + windows) 34 failed / 1638 completed — the recorded §5.2 jmltk gaps; matches the local full suite (1638/34) exactly pre-existing, documented
unit-tests key.core.infflow (both) 3 × InfFlowProfileLoadingTest pre-existing, documented
unit-tests keyext.caching (both) ProofCachingSingleRunTest.cachingClosesInASingleRun ("cache source proof must close") + TestReferenceSearcher.testFindsReferenceInSameProof pre-existing (present at slice-3-era runs)
unit-tests keyext.slicing (both) 5 tests, incl. EndToEndTests proof-size collapse (expected 5308 nodes, got 54; 3245 → 35) pre-existing (present at slice-3-era runs)
integration-tests testMt2w / testMt4w / testMtStress ProblemLoaderException → BuildingExceptions when loading JML-bearing proofs; MtScriptStressTest, RunSmallProofsMt*Test, ScDeterminismTest pre-existing
integration-tests testRunAllInfProofs / testRunAllWdProofs proofs don't close (RunAllProofsTest.java:91) pre-existing

The Phase-4 slice verification itself is unaffected: speclang slice 285/0/12,
module suites (wd 1/0/0, testgen 15/2/0, ui 44/1/0, infflow 4/3 = the 3 known
pre-existing), and the full :key.core:test run = the same 34 recorded §5.2
gaps, zero new regressions.

@wadoon

wadoon commented Oct 25, 2026

Copy link
Copy Markdown
Member Author

Branch history rewritten — every commit is now spotless-clean

I reformatted the whole branch with scripts/format_branch_with_spotless.sh
(git filter-branch --tree-filter 'gradle --parallel spotlessApply && git clean -ffdx'
over 44ad23c9e5..HEAD) and force-pushed. Summary:

  • 34 → 33 commits. The standalone spotless: fix formatting fix-up commit is
    dropped: since every commit is now formatted in place, a separate fix-up
    was redundant (the --commit-filter skips it).
  • Every commit passes spotlessJavaCheck. The formatting CI job
    (CodeQuality) no longer needs a fix-up commit anywhere in the history.
  • Final tree identical to the pre-rewrite tree (verified: 0 file diffs).
  • Base commit 44ad23c9e5 intact; commits that were already formatted keep
    their original SHAs (everything up to Phase 0, plus e50d64b699-era commits
    that were written spotless-clean). Only commits with formatting violations
    got new SHAs — this post and the PR body reference the rewritten SHAs.
  • New head: f8a64c82a4 (latest docs commit on top of rewritten fd12879b39).

The slice table in the PR description has been updated to the rewritten commit
IDs. The pre-rewrite history backup branch (jmltk-jml-pre-spotless) has been
deleted; the clone-side refs/original ref remains for a short while if a
rollback is ever needed.

CI checks re-ran automatically on the force-push; the only newly fixed job is
formatting — the other red jobs (key.core unit tests, caching, slicing,
infflow, integration proofs) are pre-existing and unchanged (see the earlier triage post).

@wadoon
wadoon removed this pull request from stack #3998 October 25, 2026 22:10
Base automatically changed from weigl/jmltk to main October 25, 2026 23:41
@wadoon
wadoon force-pushed the weigl/jmltk-jml branch 3 times, most recently from 1afd808 to 048f75d Compare October 28, 2026 22:31
@wadoon

wadoon commented Oct 29, 2026

Copy link
Copy Markdown
Member Author

The current gap between JMLtk and KeY.


Here are authentic examples for each entry, straight from the repo's fixtures, grouped by what JMLtk does with them.

1. Hard rejects — JMLtk has no grammar for these

Comment-form merge_point (testcase/merge/Gcd.java:38-45, A.java:17)

//@ merge_point
//@ merge_proc "MergeByPredicateAbstraction"
//@ merge_params {conjunctive: (int x) -> {x >= 0, (x == \old(b) || x == -\old(b))}};

JMLtk knows only the raw Java statement merge_point(expr);. This comment form dies in JmlProcessor with "Could not handle the JML comment" — even the plain //@ merge_point;.

\by {…} assertion-proof scripts (scripts/jml/NestedAssert.java:5-8)

/*@ assert x > 2 \by {
      assert x == 7 \by { assume x == 7; auto; }
      auto;
} */

\seq inside an embedded DL expression (nparser/fix3452/fix/Issue3452Fixture.java:6-7)

@ public no_state static model \seq b(\bigint n) {
@   return n == 0 ? \seq_empty : \seq_concat(b(n-1), \seq(0));

JMLtk: "In an embedded DL expression, \seq is not a known DL function name."

Comma-separated represents (key.ui/examples/heap/simple/src/MySubclass.java:4, heap/list/src/ArrayList.java:6)

//@ represents footprint = this.*, o.*;
//@ private represents footprint = array, array[*], size;

Record-declaration-level /*@ nullable */ (pipelineTests/records/input/RecordOverwriting.java:1)

/*@ nullable */ record MyRecord(String test) { ... }

"Not JML construct supported" — only component-level (per-parameter) /*@ nullable */ is fine.

spec_*_math class pragmas (testcase/specMath/java/Test.java:5)

/*@ spec_java_math @*/
public class Test { ... }

JMLtk's Modifiers() knows code_*_math, pure, nullable_by_default, … — but no spec_*_math.

KeY's label-after-assert form

//@ assert LAB: e;      // KeY's JmlParser.g4 assertion form
//@ LAB: assert e;      // JML standard prefix form — the only one JMLtk accepts

2. Silently mis-parses — worse than a clean error

Assignment = inside a JML expression (exceptional/WrongEquals.java:18-21)

/*@ public normal_behaviour
  @   ensures \result = max;
  @*/

Previously KeY's own grammar rejected this with "Assignment operator '=' is not allowed in a JML specification expression" + position. JMLtk accepts it as an assignment expression, so KeY must now catch it structurally later (via JmlSideEffectChecker) instead of at parse time.

Datagroup / monitor clauses — tokens absent → spurious declaration (monitors_for probe)

//@ monitors_for g;      // silently mis-parses into a JmlFieldDeclaration("g")
//@ readable_if g;       // same
//@ writable_if g;  in_group g;  maps_into g;  datagroup g;

3. Absent tokens (clean-ish reject), KeY's grammar had them

//@ nowarn;                                    // pragma-statement
//@ debug;                                     // pragma-statement
//@ assignable \strictly_nothing;              // no token; \nothing/\everything also become plain NameExpr
//@ captures o;                                // clause keyword absent
//@ requires ...;  //@ when ...;               // when / duration / working_space / spec_name all absent

4. Parses fine, but semantics differ (proof-level follow-ons)

\sum comprehension (exceptional/SumClause.java:11)

@   ensures \result == (\sum int i; 0 <= i && i < a.length; a[i]);

JMLtk parses it, but \sum-comprehension proofs no longer close in the merged front-end (ScDeterminismTest, RealProofMtEquivalenceTest, TestProofStarter), and labeled break k;/continue l; proofs (LoopScopeInvRuleTests) behave differently too.


Want these written into the branch as a standalone JMLTK_GAPS.md with the fixture paths as references, a minimal before/after diff per construct, or a runnable .key/.java demo per case?

- setProcessJml(true): JmlProcessor attaches JmlContract/JmlClassLevelDeclaration/
  JmlExpressionStmt directly to the Java AST
- setJmlKeys: map KeY's +KEY/-KEY/+OPENJML marker policy onto JMLtk key sets
  from GeneralSettings.getJmlEnabledKeys()
- setKeepJmlDocs(true): transitional — legacy JMLTransformer still reads raw
  JmlDoc text; removed once JMLTransformer is gone (Phase 3)
Remove the ANTLR-based JML pre-parsing front-end and consume JMLtk's
parsed AST (com.github.javaparser.ast.jml.*) directly, keeping KeY's
specification translation semantics.

Main changes:
- New JmlSpec record wraps a JMLtk AST node (Node); KeY declarations,
  statement blocks and loops carry ImmutableList<JmlSpec>/List<JmlSpec>
  as attached JML (KEY_LOOP_SPEC deleted; merged getSpec on loops).
- JMLTransformer rewritten: JMLtk already attaches parsed JML to the
  Java AST, so the transformer only relocates the nodes into the
  KEY_SPEC_CASE/KEY_CLASS_SPEC data keys, unwraps JmlMethodDeclaration/
  JmlFieldDeclaration, and self-marks JML statements (assert/assume/set
  as JmlExpressionStmt, ghost vars, KeyMergePointStmt).
- JMLSpecExtractor rewritten: dispatches JmlSpecs to the factory
  (class specs via node instanceof, method specs per-spec); keeps
  createNonNullPositionedString/arrayDepth.
- JMLSpecFactory: new JmlSpec overloads for all entry points plus the
  implicit-clause enrichment (purity assignables, \inv requires/
  ensures/signals, non-null pre/post, default signals-only, synthetic
  MODEL_BEHAVIOR for model methods) moved from the old extractor.
- New JmlToTextual re-serializes JMLtk nodes against KeY's JML grammar
  (JmlParser.g4) rule by rule and hands the parse trees to the
  KeY-specific translation.
- JP2KeYConverter: JmlExpressionStmt and KeyMergePointStmt are
  translated directly (re-parsed with the KeY grammar); loops use the
  merged spec list.
- SpecialJavaPrinter prints JmlSpec nodes; JMLInfoExtractor returns
  ImmutableList<JmlSpec>.

Known degradation (accepted): JMLtk has no statement-level merge
support, so parametric merge (merge_proc/merge_params) only works via
the KeY statement-level re-parse of the KeyMergePointStmt expression.

Compiles: :key.core:compileJava GREEN.
wadoon added 21 commits November 2, 2026 03:17
…of SpecificationRepository into new Services-owned ContractRegistry

Move the entire contract machinery out of de.uka.ilkd.key.proof.mgt.SpecificationRepository
into a new de.uka.ilkd.key.speclang.ContractRegistry (owned by Services, created via the new
Profile.createContractRegistry(Services) hook, shared by Services.copy like the spec repo /
proof registry / class-axiom computer):

- ContractRegistry holds the contract maps (contracts/operationContracts/contractsByName/
  contractTargets per (KeYJavaType,IObserverFunction) node), the contract id counters,
  initially clauses and the resolution helpers (getOverridingTargets/getInheritedContracts/
  prepareContract/splitContract/combineOperationContracts/getContractByName/map).
- WdProfile now returns ContractRegistryWD (de.uka.ilkd.key.wd) which absorbs the WD hooks
  from SpecificationRepositoryWD: wdChecks map, register/unregisterContract overrides,
  registerWdCheck/unregisterWdCheck, getWdChecks/getAllWdChecks/getWdMethodChecks (now
  package-private)/getWdClassChecks, WD getContracts/getAllContracts filter, map override,
  addWdStatement, and package-private bridges for ClassAxiomComputerWD.
- CONTRACT_COMBINATION_MARKER ("§") moved to ContractFactory; references updated in
  BlockContractImpl/LoopContractImpl/FunctionalAuxiliaryContract/ProofRegistry.
- Initially-clause machinery (createContractsFromInitiallyClauses/addInitiallyClause(s)) moves
  into the registry in this slice; it leaves for JMLSpecFactory in a later slice.
- SpecificationRepository shrinks to a thin dispatcher: ctor, map (registry + class-axiom
  computer), addSpecs (Contract/InitiallyClause -> registry; ClassInvariant/ClassAxiom ->
  computer; loop/block/merge -> AST nodes) and the empty processJavaType hook.
  SpecificationRepositoryWD keeps only the WD processJavaType override.
- TacletPOGenerator.generate now takes Services instead of SpecificationRepository (its only
  impl WDTacletGenerator reads getAllWdChecks() from (ContractRegistryWD) services.getContractRegistry()).
- All callers rewired (load pipeline SLEnvInput/KeYFile, PO loaders, rules, replay, UI dialogs,
  proofmanagement console checks, example main, test helpers); ProofRegistry splitContract/
  getOverridingTargets/marker and ClassAxiomComputer.getOperationContracts delegation switched
  to the registry.

Verified: :key.core(+wd/testgen/infflow/proofmanagement/ui/example):compileJava green;
speclang slice + IntSemanticsTest 285/0/12 (recorded slice-5 baseline); key.core.wd 1/0/0,
key.core.testgen 15/2/0, key.ui 44/1/0 all green. key.core.infflow:test has 3 pre-existing
InfFlowProfileLoadingTest failures (verified identical at Phase-3 HEAD d9ead21 and at
slice-5 HEAD — stale bracket-vs-paren contract naming in the example, unrelated to this slice).
No test code references deleted repository methods.
…rvices) in HANDOFF/PLAN

- HANDOFF §1: HEAD has the first six Phase-4 slices; repo is now a thin
  dispatcher (map/addSpecs/empty processJavaType).
- HANDOFF commits table + §7.3 step 3: slice 6 landed as 005320b, with the
  slice-6 verification record (speclang 285/0/12 + IntSemantics 6/0/0,
  key.core.wd 1/0/0, key.core.testgen 15/2/0, key.ui 44/1/0; the 3
  InfFlowProfileLoadingTest failures pre-existing, A/B-verified at Phase-3 and
  slice-5 HEADs). NEXT = slice 7 (initially clauses → JMLSpecFactory load path).
- PLAN §Phase 4: progress block now slices 1-6 DONE; contract-registry,
  combination/split, ids and inheritance table rows marked ✅ (moved to
  ContractRegistry / CONTRACT_COMBINATION_MARKER → ContractFactory); initially
  clauses row marked  in-flight (registry this slice, JMLSpecFactory next).
…JMLSpecFactory load path

Move the initially-clause machinery from de.uka.ilkd.key.speclang.ContractRegistry
into the JMLSpecFactory, which is where the constructor-postcondition derivation
belongs (its ContractFactory.cf already translated initially clauses via
initiallyClauseToContract):

- JMLSpecFactory gains the initiallyClauses per-type map plus
  addInitiallyClause/addInitiallyClauses (collection during loading) and
  createContractsFromInitiallyClauses()/createContractsFromInitiallyClause
  (derivation, called once after all contracts are registered; the derived
  constructor contracts are registered through services.getContractRegistry()
  via addContractNoInheritance/unregisterContract/cf.addPost/cf.func — the WD
  registry's registerContract override keeps creating the WD checks).
- ContractRegistry drops the initiallyClauses field, addInitiallyClause(s),
  createContractsFromInitiallyClauses and the private derivation helper; its
  map(...) no longer maps initially clauses. unregisterContract becomes public
  so the factory can unregister replaced constructor contracts (WD subclass
  override bumped to public accordingly).
- SpecificationRepository.addSpecs no longer dispatches InitiallyClause (the
  repo is Contract/ClassInvariant/ClassAxiom/loop/block/merge only now); the
  load pipeline (SLEnvInput.createSpecs, which already owns a JMLSpecFactory
  for the transform pass) partitions InitiallyClause out of the class specs,
  feeds them to the factory, and calls createContractsFromInitiallyClauses()
  at the end of loading — same position and ordering as before.

Verified: :key.core(+wd/testgen/infflow/proofmanagement/ui/example) main+test
compileJava green; speclang slice + IntSemanticsTest 285/0/12 (recorded
baseline); key.core.testgen 15/2/0, key.core.wd 1/0/0, key.ui 44/1/0 all
green; key.core.infflow 4/3 fail = the 3 pre-existing InfFlowProfileLoadingTest
failures only (stale bracket-vs-paren contract naming in the example, verified
pre-existing at Phase-3/slice-5 HEADs). One testgen run hit an environment OOM
(exit 137, heavy concurrent load incl. the silent-wizard key.ui run); rerun
solo passed. No test code references the removed methods.
… path) in HANDOFF/PLAN

- HANDOFF §1: HEAD has the first seven Phase-4 slices; repo is still a thin
  dispatcher (map/addSpecs/empty processJavaType); NEXT = addSpecs collapse +
  map(...) removal.
- HANDOFF commits table + §7.3 step 3: slice 7 landed as fd32802 with the
  slice-7 verification record (all main+test compile green incl. the one WD
  visibility-bump for the now-public unregisterContract; speclang+IntSemantics
  285/0/12; module suites run solo after a combined run OOM'd (exit 137) —
  key.core.testgen 15/2/0, key.core.wd 1/0/0, key.ui 44/1/0; infflow 4/3 = the
  3 pre-existing InfFlowProfileLoadingTest failures only). NEXT = slice 8
  (addSpecs collapse + map removal), then repo deletion.
- PLAN §Phase 4: progress block now slices 1-7 DONE; initially-clauses table
  row marked DONE (factory collects at load, derives constructor contracts at
  end of loading, same ordering).
… from the repo/registry/computer surface

SpecificationRepository is now down to its constructor and the empty per-type
processJavaType hook (the last slice deletes it together with the Services/
KeYEnvironment accessors, Profile.createSpecificationRepository, WdProfile
override and SpecificationRepositoryWD):

- SLEnvInput.createSpecs: the addSpecs dispatch collapses into a private
  registerSpecs helper (formerly SpecificationRepository.addSpecs) that routes
  each element verbatim: Contract → services.getContractRegistry().addContract,
  ClassInvariant/ClassAxiom → services.getClassAxiomComputer().add/invariants,
  LoopSpecification/BlockContract/LoopContract/MergeContract → their AST nodes.
  All three load sites (class specs, method specs, constructor specs) call it
  directly with initConfig.getServices() — same iteration order as before, so
  contract-id assignment is unchanged.
- SpecificationRepository.map, ContractRegistry.map + the mapValueSets helper,
  ClassAxiomComputer.map + its mapValueSets helper, and the ContractRegistryWD
  map override (wdChecks remap) are deleted: the top-level walkers had zero
  callers anywhere in main or test code (the per-element
  SpecificationElement.map(UnaryOperator, Services) API itself stays — the WD
  WellDefinedness implementations still use it recursively). The load-time
  at-pre/WD transforms re-walk the AST-attached specs instead of the global
  snapshot.
- ContractRegistry thread-safety comment updated: maps are populated during
  problem loading and only read afterwards — no state is mutated during
  proving anymore (the synchronized-map rationale is gone with map(...)).

Verified: :key.core(+wd/testgen/infflow/proofmanagement/ui/example) main+test
compileJava green; speclang slice + IntSemanticsTest 285/0/12 (recorded
baseline); module suites solo: key.core.testgen 15/2/0, key.core.wd 1/0/0,
key.ui 44/1/0 green; key.core.infflow 4/3 = only the 3 pre-existing
InfFlowProfileLoadingTest failures. Regenerated test fixture TestGeneric0.java
restored after the testgen run. No test code references the removed methods.
The last remaining piece of SpecificationRepository, the per-type
processJavaType hook, moves into the Services-owned contract registry, and the
repository plus its accessors are deleted:

- ContractRegistry gains an empty processJavaType(KeYJavaType) hook (the
  profile-created home for per-type post-processing, like the repository was);
  ContractRegistryWD overrides it with the body formerly in
  SpecificationRepositoryWD (RepresentsAxiom -> MethodWellDefinedness /
  addRepresents processing; the cast to ContractRegistryWD collapses to this).
- SLEnvInput.createSpecs calls contractRegistry.processJavaType(kjt) instead
  of specRepos.processJavaType(kjt); the specRepos variable and the
  SpecificationRepository import are gone.
- Deleted: SpecificationRepository.java and SpecificationRepositoryWD.java
  (key.core.wd), Service.specRepos field + getSpecificationRepository() +
  copy paths, KeYEnvironment.getSpecificationRepository(),
  Profile.createSpecificationRepository (default) + WdProfile override, and
  the SpecificationRepository imports in Services/KeYEnvironment/Profile/
  WdProfile/ContractFactory (ContractFactory/ProofRegistry javadoc mentions
  downgraded to plain-text so no dangling @link remains).

Phase 4 complete: the repository is gone; all specifications live on the AST
nodes or in the Services-owned components (ContractRegistry, ClassAxiomComputer,
ProofRegistry, JMLSpecFactory).

Verified: :key.core(+wd/testgen/infflow/proofmanagement/ui/example) main+test
compileJava green; speclang slice + IntSemanticsTest 285/0/12 (recorded
baseline); module suites solo: key.core.testgen 15/2/0, key.core.wd 1/0/0,
key.ui 44/1/0 green; key.core.infflow 4/3 = only the 3 pre-existing
InfFlowProfileLoadingTest failures. Regenerated test fixture TestGeneric0.java
restored after the testgen run. No test or main code references the deleted
class or its methods.
…moval, SpecificationRepository deleted) in HANDOFF/PLAN

Phase 4 is finished: SpecificationRepository.java no longer exists.

- HANDOFF §1: HEAD contains all nine Phase-4 slices; SpecificationRepository
  is DELETED (e50d64b); specs live on the AST or in the Services-owned
  ContractRegistry/ClassAxiomComputer/ProofRegistry/JMLSpecFactory.
- HANDOFF commits table + §7.3: slices 8 (2ae5b95: addSpecs collapse into
  SLEnvInput.registerSpecs + map(...)/mapValueSets removal from the
  repo/registry/computer surface; repo shrinks to ctor + processJavaType) and
  9 (e50d64b: repo + Services/KeYEnvironment accessors +
  Profile.createSpecificationRepository + WdProfile override +
  SpecificationRepositoryWD deleted; processJavaType -> ContractRegistry/WD)
  with their verification records (speclang+IntSemantics 285/0/12, module
  suites solo, infflow 4/3 = the 3 pre-existing failures). §7.4 notes no
  Phase-4 work remains.
- PLAN §Phase 4: progress block now says Phase 4 COMPLETE (slices 1-9); the
  map/addSpecs/thread-safety table rows marked DONE; the deliverable note
  marked REACHED; the ProofRegistry row's historical 'like specRepos' wording
  fixed.
…maining red jobs verified pre-existing (caching/slicing/infflow/integration proofs unchanged since slice-3-era commits)
…per-commit)

weigl/jmltk-jml was rewritten with scripts/format_branch_with_spotless.sh:
tree-filter 'gradle --parallel spotlessApply && git clean -ffdx' plus a
commit-filter dropping the now-redundant 'spotless: fix formatting'
fix-up commit (34 -> 33 commits). Every commit is spotless-clean, the
final tree is identical to the pre-rewrite tree, and base 44ad23c is
intact.

Update HANDOFF/PLAN commit references to the rewritten SHAs and reword
the CI-formatting notes (formatting fold, no e17111e).
JMLtk 1.1.0-SNAPSHOT no longer ships the KeY-specific
getAssociatedSpecificationComments()/attachedJml carrier. Read the parsed
JML specifications directly from the typed JMLtk AST instead:

- Java AST classes: remove the attachedJml carrier and
  getAssociatedSpecificationComments(); createComments now returns
  Collections.emptyList().
- KeYJPMapping.registerReverse: register reverse (KeY -> JMLtk) mappings
  for classes (JmlTypeDeclaration) and callables (contract carrier).
- JP2KeYConverter: keep the contract carrier for each KeY method via the
  reverse mapping; skip pure class-level JML constructs (invariant,
  initially, axiom, represents, accessible) when converting members.
- JMLSpecExtractor: rewritten to read contracts, model method
  definitions, class-level specs and loop invariants from the JMLtk AST
  via the mapping.
- JmlToTextual/JMLSpecFactory: overloads building spec texts from the
  typed classes (JmlContract, JmlMethodDeclaration, ...) and direct
  conversion of JmlContract/JmlMethodDefinition.
- JMLTransformer: unwrap JmlMethodDeclaration/JmlFieldDeclaration
  wrappers into the member list again, since Javaparser symbol
  resolution ignores wrapped declarations; the wrapper is remembered on
  the inner node via the new KEY_JML_WRAPPER data key and is used as the
  contract carrier (model method definition, ghost field).
- Delete JmlSpec.java.
…front-end

The rebase onto jmltk 1.1.0-SNAPSHOT replaced the intermediate JmlSpec
carrier/attachedJml design with a carrier-free one: KeYJPMapping reverse
mappings are the only bridge between the ASTs and JMLSpecExtractor reads
contracts, model method definitions, class-level specs, and loop/block
contracts back from the JMLtk typed AST.

- PLAN.md: final status header; §2/§2.2 pipeline diagram and §4 rewritten to
  the shipped architecture; Phase 0/1/2 marked DONE/SUPERSEDED; §7 Q1-2
  resolved; §8 replaced by the final build status.
- JML_MAPPING.md: header + Part A (producers/carriers/consumers) rewritten to
  the shipped design; Part E updated (no carrier between the ASTs).
- HANDOFF.md: header + §1 mark the completion (HEAD d2b2112, 279/0/12
  speclang slice, whole-build compile + spotless green); §2 rewritten to the
  final front-end; §3 model-method detection updated; §7 resume list closed.
Close the paths in the JMLtk-based JML front-end where a construct that
JMLtk accepts could be silently dropped, weakened, or mis-translated on
its way through KeY's own JML grammar and term translation.

- JmlToTextual: hard-fail on untranslatable clauses instead of
  catch-and-skip. The KNOWN_GAPS allow-list starts empty; a clause is
  only skippable after its loss is triaged as acceptable. Unknown class
  level construct kinds are rejected loudly instead of skipped. The
  JmlSideEffectChecker runs on every translated contract and clause.

- JmlSideEffectChecker: new structural AST guard (not a text scan) that
  rejects assignment operators, ++/--, object creation, and array
  creation inside JML specification expressions. set statements and
  ghost declarators are exempt (assignment is their point), and the
  visitor never descends into Java bodies. Wired into JmlToTextual
  (contracts, loop specs, class level clauses) and the assert/assume
  branch of JP2KeYConverter.

- JMLTransformer: class level "field-like" clauses that JMLtk models as
  JmlFieldDeclaration with a bogus type (the keyword) can no longer
  become spurious Java fields. The datagroup family (datagroup,
  in_group, maps_into, monitors_for, readable_if, writable_if,
  mapped_by, repr) is rejected loudly; captures/duration/working_space
  remain accepted drops of deprecated legacy clauses.

- JmlDocRemoval: a `name "..."` clause that JMLtk cannot parse would
  silently drop the whole spec case containing it; such leftover JmlDoc
  nodes are now rejected loudly (word-boundary guarded so `spec_name`
  clauses stay accepted drops).

- JmlToTextual serializers: signals clauses omit the binder name
  (`\exception`) that JMLtk synthesizes; the information-flow
  serializer is rewritten to JMLtk's shifted getters (getBy ->
  getDeclassifies -> getErases -> getExpressions -> getNewObjects),
  emits the terminating `;`, wires loop_determines/loop_separates to
  KeY's loop clause grammar, and rejects a \declassifies/\erases
  section that the loop clause grammar cannot express.

- Parallel-prover merge-contract safety: synchronized contract
  accessors on StatementBlock/LoopStatement/MergePointStatement and a
  new atomic MergePointStatement.setMergeContracts; ProgVarReplaceVisitor
  and IntroAtPreDefsOp use it instead of the racy removeMergeContracts()
  + addMergeContract() sequence so concurrent rule application never
  observes an empty contract set.

- Assertion proof scripts (`assert e \by { ... }`): JP2KeYConverter
  re-parses the JMLtk JmlProofStmt list against KeY's assertionProof
  grammar into KeyAst.JMLProofScript instead of dropping the script; a
  script JMLtk accepted but KeY cannot re-parse fails loudly. Applies
  to assert and assume (new AssertionProofScriptTest).

- Quantifier round-trip fixtures: SumClause and BsumClause lock that
  \sum and \bsum (canonical `(\bsum T v; from; to; summand)` range
  form) survive the JMLtk -> KeY re-parse verbatim; \bsum keeps its
  loud deprecation warning in JmlTermFactory.

- Spec math modes: restore the class level spec_*_math pragmas in the
  specMath fixtures (JMLtk parses them as real JML modifiers), re-enable
  specMathJavaMathTest/specMathBigintMathTest in ContractLoadingTests,
  and add SpecMathModeTest covering class, method, and inline
  model-method modes.

- JMLtk snapshot pin: build.gradle comment documents why the 1.1.0-
  SNAPSHOT snapshot repository is kept (release 1.0.0 lacks the
  ast.jml.proof package) and tracks jmltoolkit/jmltk#82 for pinning.

New exceptional-fixture guards (JMLParserExceptionTest): WrongEquals is
now a loud JmlSideEffectException (assignment in an ensures clause),
plus IncrementInContract, ObjectCreationInContract, NoSideEffects,
Datagroup, DeprecatedDrops, NameClause, and NameClauseClass.
JMLtk's grammar cannot parse KeY extension statements inside //@ comments
(merge point statements, set statements, and assert/assume statements with
KeY proof scripts or obtain clauses). Instead of dropping them, re-parse the
leftover comment with KeY's own JmlParser, mark the JmlDocStmt with the
parsed methodlevel_element (KEY_JML_FALLBACK), and translate it in the
converter.

- JMLTransformer: re-parses unhandled method level JML comments and attaches
  the parse tree to the leftover JmlDocStmt.
- JavaService.unwrapParseResult: tolerates the JMLtk gap problems and
  re-injects the symbol resolver when the tolerated problems suppressed it.
- JP2KeYConverter.visit(JmlDocStmt): translates merge points, set statements
  and asserts/assumes (incl. KeY proof scripts) from the fallback parse tree.
- JMLSpecExtractor/JMLSpecFactory: derive merge contracts from the fallback
  parse tree.

Also fixes two regressions of the JMLtk front-end:

- visit(KeyMergePointStmt): keep the MergePointStatement wrapper in the
  schema (taclet pattern) branch, so deleteMergePoint matches again and
  automatic merge proving/stored-proof replay work.
- handleSpecialFunctionInvocation: translate \seq(...) sequence literals by
  concatenating singleton sequences, mirroring TermBuilder.seq. Previously
  embedded \seq(...) in model method bodies was rejected as an unknown DL
  function (Issue3452Test failed).
The rebase onto main brings the merged type-annotations feature, which
changed the TypeReference API of TermBuilder (selfVar takes TypeRef now)
and the TypeDeclaration constructor surface. Adapt the JML front-end
takeover to it:

- AnnotationInterfaceDeclaration: drop the removed TextualJMLConstruct
  jmlAttachments parameter (branch deleted the carrier in Phase 1)
- JP2KeYConverter.visit(AnnotationDeclaration): no longer pass getClassSpec
- ClassAxiomComputer: selfVar(kjt) -> selfVar(new TypeRef(kjt), ...)
…ed/changed under JMLtk

Records every JML construct/test that worked with KeY's pre-takeover
front-end but is rejected, silently mis-parsed, or behaviorally different
under the JMLtk front-end, with authentic fixture snippets, fixture paths,
and per-entry status measured at HEAD.

Notable measured findings at 048f75d:
- comment-form merge_point, \by proof scripts, \seq escape and proof-starter
  cases are RESOLVED by the KEY_JML_FALLBACK re-parse mechanism
- \seq is corrected: the old 'not a known DL function name' failure is a KeY
  name-resolution issue (JP2KeYConverter), not a JMLtk parse rejection
- KeyJavaPipelineTest is an uncovered REGRESSION: 86/136 red (0 on main),
  not part of the recorded 34-failure baseline
- remaining open gaps: comma-separated represents loads, \sum proofs,
  labeled-break proof, relax.proof replay, records pipeline, spec_*_math,
  and the absent clause/pragma/datagroup tokens
…refix

Named spec cases are written as `[behavior] <name>:` (an identifier prefix,
e.g. `public normal_behavior CaseA: ...`), the only form JMLtk parses into
JmlContract.name. JmlToTextual.toSpecCase already maps it via addName to the
spec-case name and JMLSpecFactory.generateName to the operation contract
base name, so no translation change is needed - only the source form and
the guard fixtures.

- JmlDocRemoval: the legacy `name "..."` clause stays a loud error (JMLtk
  cannot parse it and it would silently drop the whole spec case), but the
  message now points to the replacement `[behavior] <name>:` prefix form.
- NameClause (exceptional, prev. expected ProofInputException 'Case A'):
  becomes the positive fixture testcase/speclang/njml/NameClause.java using
  the new form; ContractLoadingTests.nameClauseCarriesContractName asserts
  the operation contract base name is 'CaseA'.
- NameClauseClass (class level legacy `name "..."`): still a loud error;
  comment documents that class-level spec naming has no JMLtk counterpart.
- JMLTK_GAPS.md row 12: `name` is replaced (RESOLVED), not lost.
The record goldens were generated by the old PreParser-based attachSpec and
no longer matched the current pipeline output. With the JMLtk front-end the
generated specs are printed faithfully, so the goldens are regenerated from
the branch's fresh 'actual/' output (102 -> 108 files, 136 -> 142 tests).

Semantic differences in the generated code/specs:

- hashCode() == o.hashCode() ==> !\result  ->  hashCode() != o.hashCode() ==> !\result
  and 'o == null ==> !\result' -> 'o != null ==> !\result' (stages 05-16, 18):
  the old forms came from the PreParser's re-serialization; JMLtk prints the
  RecordClassBuilder template (which already used '!='/ '!=') truthfully.
- removed '[[JML modifiers]]' placeholder markers in early stages (01-06);
  JMLtk records real modifiers instead.
- nullable annotations are kept inline ('/*@ nullable */') instead of being
  detached into leading comments (01-06).
- new stage 17_AnnotationMover with 6 golden files (the stage exists on the
  branch but had no goldens yet).

The test is byte-exact: expected/ is identical to a fresh actual/ run
(108/108), KeyJavaPipelineTest is green (142/0).
… HEAD

- HANDOFF §5.2 UPDATE block: rows (a) merge family, (b) JmlScriptTest, (c)
  Issue3452Test, (f) TestProofStarter are green at HEAD (measured 2161/0);
  row (j) KeyJavaPipelineTest re-measured as 86 failures (stale record
  goldens), now green 142/0 after the regenerated goldens; points to
  JMLTK_GAPS.md as the living record. Remaining open: 12 (rows d/e/g/h/i).
- JMLTK_GAPS §6: status flipped from REGRESSION to RESOLVED — record goldens
  deliberately regenerated (expected 102 -> 108 files incl. 17_AnnotationMover),
  KeyJavaPipelineTest 142/0, byte-exact vs fresh actual.
…al fixtures

- JMLParserExceptionTest fixtures: NowarnDebug (class-level loud
  ProofInputException) and NowarnDebugInMethod (method-level drop via the
  JmlDocStmt fallback) pin the JMLtk-absent pragma-statements.
- MonitorsFor/ReadableIf/WritableIf/InGroup/MapsInto lock the JMLTransformer
  DATAGROUP_FAMILY guard per keyword: loud IllegalArgumentException, no
  spurious fields.
- JMLTK_GAPS.md §13/§14 + status table, JML_MAPPING.md gaps 4-5 and PLAN.md
  item 5 updated to the measured state at this commit.
- Re-ran the Appendix A suite at 8fe1be5 (192 tests, 12 failed) and
  re-confirmed every recorded count: merge family 12/12 (+3
  PredicateAbstractionLatticeTests), JmlScriptTest 7/7, Issue3452Test 2/2,
  ScDeterminismTest 4 load + 3 proof failures, RealProofMtEquivalenceTest 1,
  KeyJavaPipelineTest 142/0, LoopScopeInvRuleTests 1, 2 replayer failures,
  ParallelProverDeterminismTest 1 (flags have to have the same size).
- Header now records measurement at the rebased tip 8fe1be5 (Nov 2 2026).
- Remap rewritten commit hashes: 048f75d->bf7048ca5d, edbed71->
  251f945, 10f4bfc->841b999eee.
- Fixture path corrections: arrayMax.key and polishFlagSort.key live under
  standard_key/java_dl (not heap/comprehensions respectively heap/); exact
  heap/{list,simple} prefixes for the four ScDeterminismTest load files.
The doc still recorded the pre-remediation state (pragmas removed, two load
tests @disabled). At 8fe1be5 the fixtures carry their spec_java_math/
spec_bigint_math pragmas again and the R4 mapping via ModifierKind.JML_SPEC_*
to SpecMathMode is in place: SpecMathModeTest (2) and ContractLoadingTests
(6, incl. specMathJavaMathTest/specMathBigintMathTest) are green.
…loop nodes

The loop-invariant taclets (loopScopeInvDia & co.) bind #loopStmt via
\storeStmtIn, which instantiates the taclet's pattern template
'while (#nse) #body' and thus yields a *fresh* LoopStatement (position
UNDEFINED) that is only structurally equal to the loop in the goal. The
pre-JMLtk SpecificationRepository resolved specs by structural (loop,
line) key, so the fresh node still found its invariant; the node-attached
loopInvariant field is null on the fresh node and the rules failed to
match ('rule loopScopeInvDia not available or not applicable'), breaking
replay of any saved proof applying a loop-invariant rule (e.g.
reverseArray.proof node 90).

Add LoopStatement.getLoopInvariant(Statement program): when the node
itself carries no invariant, search the given program for a structurally
equal loop that does (mirroring the old map semantics) and use it. Apply
the fallback in the four loop varcond classes (hasInvariant,
getInvariant, getFreeInvariant, getVariant) and in
MiscTools.getSpecForTermWithLoopStmt (CreateHeapAnonUpdate,
CreateFrameCond), which suffered the same regression on the stored
loopFormula term.
Three independent drifts between the JMLtk front-end and the retired
PreParser/TextualJML front-end changed the loaded KeY program or its
specifications enough to break replay of saved proofs (and, with it, the
slicing and caching suites that load and re-prove stored proofs).

1. Model/ghost methods (JP2KeYConverter). A method unwrapped from a
   JmlMethodDeclaration carried its JML definition as an executable body,
   so e.g. `\old`-containing model-method text was built as Java code
   ("In an embedded DL expression, \old is not a known DL function
   name."). The pre-JMLtk front-end produced signature-only KeY methods
   for these (TextualJMLMethodDecl.getParsableDeclaration emits `...;`);
   the definition is a specification, translated via the JML grammar
   (JmlToTextual.toMethodDecl, AXIOMS). Convert the body as empty when a
   JML wrapper is present.

2. Model-method contracts (JMLSpecExtractor). JMLtk attaches the contract
   to the inner MethodDeclaration (a NodeWithContracts), while
   JmlMethodDeclaration.getContract() is empty for them; the extractor
   only read the wrapper, so the contract was lost and the fallback built
   a trivial `true` contract, leaving the model method without a
   `requires`/`ensures` axiom (replay then fails on
   `Contract_axiom_for_..._in_...`). Collect from both carriers.

3. JML field initializers (JMLTransformer). JMLtk keeps a JML field
   declaration's initializer on the wrapped FieldDeclaration; the
   pre-JMLtk front-end materialized class-level JML fields without
   initializers, so they never reached the constructors. The kept
   initializer (e.g. `//@ ghost ... = null` in java.lang.Throwable) was
   inlined into the synthetic `$init`, changing the method body and
   breaking `methodCallEmpty` in stored proofs. Strip initializers when
   unwrapping JmlFieldDeclaration.

Verified: keyext.slicing 23/23 green (sliceJavaProof,
sliceMultipleIterations, Issue3834Test, IssueRenamingTest,
ProofListenerIsolationTest).
JMLtk 1.2.1 parses the comment-form merge point (`//@ merge_point ...`)
into a new typed statement node `JmlMergePointStmt`
(`com.github.javaparser.ast.jml.stmt`, added in this release) instead of
leaving it as an unparsed leftover comment. The converter had no handler
for it, so the default visitor walked the node's children and aborted on
the JML key tag `SimpleName` ("Unsupported element detected ...
SimpleName"), breaking the load of every proof with a merge point
(MergeRuleTests 9x "Load failed"). A/B-confirmed: green at 1.1.0, red at
1.2.1, otherwise identical code.

- JP2KeYConverter: visit(JmlMergePointStmt) -> a MergePointStatement
  holding a temporary boolean location variable, mirroring the re-parsed
  fallback form (the node carries no executable expression).
- JMLSpecExtractor: route a mapped JmlMergePointStmt to the merge-contract
  factory.
- JMLSpecFactory: new createJMLMergeContracts overload that re-serializes
  the node's optional merge procedure / lattice / predicate metadata to the
  textual `merge_point` statement and re-parses it with KeY's JML grammar,
  reusing the shared translation (so proc and merge_params are honored).

Verified under 1.2.1-SNAPSHOT: MergeRuleTests 12/12,
PredicateAbstractionLatticeTests 3/3; key.core recorded-baseline re-run
shows no new failures.
JMLTK_GAPS.md:
- Re-measured at 53dcefd (jmltk 1.2.1-SNAPSHOT + the typed merge-point
  handler): key.core red count is 11 (was 14); the loop-invariant
  structural-lookup fix (6a07c20) resolved 3 recorded reds.
- S5: arrayMax.key/polishFlagSort.key were loop-invariant regressions, not
  \sum-fidelity failures — corrected and removed from the gap.
- S9: marked RESOLVED with the root cause.
- S10: extended with keyext.caching TestReferenceSearcher and the node-40
  root cause (assignable \strictly_nothing postcondition frame).
- S1: recorded the 1.2.1 typed JmlMergePointStmt handling (53dcefd).
- New S17: [no file] in origin term labels — cosmetic only (labels are
  proof-irrelevant), documented, not fixed.

TODO_CI.md: the CI red-hunt record for PR #3997 — the four stored-proof
replay root causes behind the 5 keyext.slicing failures, the keyext.caching
triage (1 fixed, 1 accounted under the recorded relax.proof gap), the jmltk
1.1.0 -> 1.2.1-SNAPSHOT bump with A/B evidence, and the final per-job state
at the branch tip.
The 2026-11-08 CI run at 4b62b1d triages the remaining red integration
jobs:
- testRunAllWdProofs is now green (was red at 2026-11-02) - resolved by the
  program/spec-shape fixes.
- testMt2w/testMt4w: the curated small-MT collections contain exactly the
  recorded store-ref/sum failures (S4/S5); no new MT prover failure.
- testMtStress: MtScriptStressTest x2 fail to LOAD heap/quicksort/sort.key -
  newly identified, pre-existing (identical failure in the 2026-11-02 CI at
  1.1.0-SNAPSHOT), previously unrecorded because the testMt*/testRunAll*
  jobs were not part of the local baseline. Root cause: JMLtk (both
  1.1.0 and 1.2.1) parses the KeY-specific ghost local
  '//@ ghost \seq x = \dl_array2seq(a);' as a class-level
  JmlFieldDeclaration and reports a hard problem that
  JavaService.unwrapParseResult does not tolerate. Recorded as S18 with fix
  options.
- TODO_CI S4 updated with the per-job triage state.
…locker

Local corpus runs of testRunAllFunProofs (22 red groups) and
testRunAllInfProofs (10 red groups) at the branch tip complete the
integration triage. origin/main's Tests workflow is fully green, so
every Fun/Inf red is PR-induced (the front-end swap); the Fun set
shrank 33 -> 22 between the 1.1.0 and 1.2.1 CI runs via the
program/spec-shape fixes.

New gap families recorded in JMLTK_GAPS.md:
- S19: JMLtk accepts the standard-JML 'ensures_redundantly' clause but
  KeY's JmlParser.g4 has no production for it, so JmlToTextual throws
  'Unsupported clause kind' and the whole source fails to load
  (SmansEtAl group).
- S20: the 'assignable_free <expr>' clause makes the converter run the
  symbol resolver on the clause keyword, an UnsolvedSymbolException
  aborts the load (javadl group).
- S21: the tracked heap/permutedSum/perm.proof still loads but its
  recorded replay now has 6 errors (stored rule position no longer fits
  the sequent, One Step Simplification not reconstructable) - a stale
  recording against the new front-end, same family as the relax.proof
  gap (S21).

S17 upgraded from cosmetic to a save->reload blocker: saved proofs
carry 'requires @ [no file]' origin strings that
OriginTermLabelFactory.parseOrigin rejects (renderer/parser
asymmetry), so reload-enabled RunAll groups (other, list_seq) fail;
fix options recorded. S4 extended with the RunAll corpus fixtures it
breaks (list, javaCard, funOfIF, simple_info_flow, simpleTests, Inf
NewObjects).
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.

1 participant