Skip to content

Generate user documentation for varconds and proof script commands - #3977

Open
wadoon wants to merge 14 commits into
mainfrom
weigl/varconddoc
Open

wadoon wants to merge 14 commits into
mainfrom
weigl/varconddoc

Conversation

@wadoon

@wadoon wadoon commented Aug 20, 2026 •

Copy link
Copy Markdown
Member

Related Issue

This pull request does not resolve a specific issue; it continues and extends the
documentation-generation feature of this branch.

Intended Change

This PR adds the generation of user documentation for the taclet language constructs —
variable conditions (varconds), proof script commands, and code transformers — and fixes the
varcond builder machinery that was broken by the initial version of this feature:

  1. Fix broken rule loading (regression on this branch): TacletBuilderCommandInfo.isNegationSupported()
    threw an IllegalStateException for varcond implementations whose constructor
    parameter types do not match the declared ArgumentType categories (e.g.
    \isConstant, \hasLabel). The exception was swallowed by TacletPBuilder and
    surfaced as semanticError("Could not apply the given variable condition"),
    which made the wd rule files (wdGeneralRules.key, wdFormulaRules.key) fail to
    load, breaking all 8 wd proofs. Negation detection is now tiered: a constructor
    whose parameter types are compatible with the declared argument types decides
    negation support by its trailing boolean parameter; only if no type-compatible
    constructor exists, the lenient fallback on the first public constructor is used.
    findConstructor (documentation lookup) prefers type-compatible constructors and
    falls back to matching by parameter count.

  2. Documentation generators for the three taclet-language construct types, built on a
    single source of truth:

    • VarcondDoc (all registered varconds), ScriptDoc (all proof script commands), and
      CodeTransformerDoc (all program and term transformers, discovered via the new
      @TransformerMeta annotation that carries the internal meta-construct name).
    • Every generator builds one common data structure — a list of DocItems with nested
      DocSignature/DocParameter records — that is both dumped verbatim as a structured
      JSON file and used to render the Markdown document (in the shared base class
      AbstractDocGenerator). This replaces the previous design with two parallel
      traversals, in which the Markdown and JSON outputs drifted apart (e.g. transformer
      parameters appeared under their javadoc names in the Markdown but under reflection
      names arg0/arg1 in the JSON). The dependency-free JsonWriter serializes
      records via their record components.
    • AbstractDocGenerator.cleanJavadoc() was overhauled: inline tags are now scanned
      brace-balanced (unbalanced braces and {@snippet} bodies are handled correctly),
      {@link}/{@linkplain} become Markdown links, and inline snippets
      ({@snippet lang="..." : ...}) are rendered as fenced code blocks.
    • All code transformers in de.uka.ilkd.key.rule.metaconstruct (including arith)
      are annotated with @TransformerMeta(name = "..."), using the internal name passed
      as the first argument of super(...).
  3. Gradle integration: the tasks :key.core:generateVarcondDoc,
    :key.core:generateScriptDoc, and :key.core:generateCodeTransformerDoc write
    varconds.md/varconds.json, script-commands.md/script-commands.json, and
    code-transformers.md/code-transformers.json to key.core/build/docs/keydocs.

  4. Nightly deployment: the nightly workflow additionally sets up JDK 25 (therapi's
    scribe only extracts /// markdown javadocs on JDK 23+), runs the three generation
    tasks, and uploads key.core/build/docs/keydocs/* as the artifact varcond-script-docs
    with a retention of 30 days.

  5. Javadoc completion: missing class and constructor documentation of the varcond
    implementation classes in de.uka.ilkd.key.rule.conditions (which feed VarcondDoc),
    gaps in de.uka.ilkd.key.scripts (which feed ScriptDoc), and the javadocs of the
    transformer classes in de.uka.ilkd.key.rule.metaconstruct (which feed
    CodeTransformerDoc) were completed, so the generated documentation is not empty.

Plan

  • Fix wd rule loading broken by strict constructor matching
  • Reintroduce the ArgumentType-vs-parameter-type check in a tiered design
  • Complete missing Javadocs in rule.conditions, scripts, and rule.metaconstruct
  • Annotate all program and term transformers with @TransformerMeta
  • Implement documentation generators with a common data structure
    (DocItem/DocSignature/DocParameter) dumped as JSON and rendering the Markdown
  • Add Gradle tasks generateVarcondDoc/generateScriptDoc/generateCodeTransformerDoc
  • Add documentation generation and artifact upload to the nightly workflow
  • Update key-docs (KeYProject/key-docs) to download the artifact into docs/user
    and add the pages to the nav

Type of pull request

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Refactoring (behaviour should not change or only minimally change)
  • There are changes to the (Java) code
  • There are changes to the deployment/CI infrastructure (gradle, github, ...)

Ensuring quality

  • I made sure that introduced/changed code is well documented (javadoc and inline comments).
  • I made sure that new/changed end-user features are well documented (https://github.com/KeYProject/key-docs).
  • I have tested the feature as follows:
    • ./gradlew :key.core.wd:testRunAllWdProofs passes 9/9 with the reintroduced type checks.
    • ./gradlew :key.core:generateVarcondDoc :key.core:generateScriptDoc :key.core:generateCodeTransformerDoc
      produce complete Markdown and JSON files (53 varconds, 39 proof script commands, 52 code
      transformers); the 52 Markdown headings match the @TransformerMeta names, and all JSON
      dumps parse as valid JSON.
    • ./gradlew :key.core:compileJava and :key.core:spotlessApply run cleanly.
  • I have checked that runtime performance has not deteriorated.
    (The reflection-based matching is only used for negation detection and documentation
    lookup, and the negation result is cached per command info.)

Additional information and contact(s)

The generated documentation is uploaded by the nightly workflow as the artifact
varcond-script-docs; the corresponding download step for the key-docs repository
(mkdocs.yml) will be contributed there separately.

The contributions within this pull request are licensed under GPLv2, or (at your option) any later version, for inclusion in KeY.

@wadoon

wadoon commented Aug 20, 2026

Copy link
Copy Markdown
Member Author

We might want to discuss whether we want to introduce this library https://github.com/dnault/therapi-runtime-javadoc

Which brings annotations and an annotation processor that writes annotations capturing the Javadoc to Clazz and other Java entities for runtime usage

@wadoon
wadoon requested a review from unp1 August 21, 2026 16:08
@wadoon wadoon self-assigned this Aug 21, 2026
@wadoon wadoon added Documentation Improvements or additions to documentation Java Pull requests that update Java code labels Aug 21, 2026
@wadoon wadoon added this to the v3.1.0 milestone Aug 21, 2026

@unp1 unp1 left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks. No blockers just discussion points.

@wadoon
wadoon force-pushed the weigl/varconddoc branch 2 times, most recently from 373f1ed to bbdd443 Compare August 23, 2026 18:49
@wadoon
wadoon requested a review from unp1 August 23, 2026 18:51

@unp1 unp1 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks! Concerning the failing WD test, that constructor for ConstantCondition takes a JAbstractSortedOperator which is not a parseable variable.

(also "\reference" does not take a variable)

Comment thread key.core/src/main/java/de/uka/ilkd/key/doc/VarcondDoc.java Outdated
Comment thread key.core/src/main/java/de/uka/ilkd/key/doc/VarcondDoc.java Outdated
Comment thread key.core/src/main/java/de/uka/ilkd/key/doc/VarcondDoc.java Outdated
@wadoon wadoon changed the title Add support for generation of varcond documentation Generate user documentation for varconds and proof script commands Oct 10, 2026
@wadoon

wadoon commented Oct 11, 2026

Copy link
Copy Markdown
Member Author

Current version:

\applyUpdateOnRigid

This variable condition can be used to check whether an update can be performed on a formula or
term.
That is the case if the top-level operator is rigid and of arity greater than 0.

Signatures

  • \applyUpdateOnRigid(u: VARIABLE, phi: VARIABLE, result: VARIABLE)

    Creates an instance of the variable condition.

    • u the schema variable matched against an update
    • phi the schema variable matched against a formula or term
    • result the schema variable containing the result of applying u on
      phi

\containsAssignment

This variable condition can be used to check whether an assignment expression occurs as a
subexpression of a schemavariable instantiation,

Signatures

  • \containsAssignment(x: VARIABLE)

  • \not\containsAssignment(x: VARIABLE)

    creates an instance of the variable condition

    • x the schemavariable whose instantiation is to be checked
    • negated true iff the check should ensure the absence of an assignment statement

\different

A variable condition for the taclet construct \different, ensuring that two schema
variables are instantiated with distinct elements.

Signatures

  • \different(var1: VARIABLE, var2: VARIABLE)

    Instantiates a new different-instantiation condition.

    • var1 the first schema variable
    • var2 the second schema variable; both must not be instantiated with the same element

\differentFields

This variable condition checks if given two terms s, t both terms have a different unique symbol
as top level operator

Signatures

  • \differentFields(var1: VARIABLE, var2: VARIABLE)

    Instantiates a new different-fields condition.

    • var1 the first schema variable; both instantiations must differ in their top-level
      unique function symbol
    • var2 the second schema variable

\disjointModuloNull

General varcond for checking relationships between types of schema variables.

Signatures

  • \disjointModuloNull(: TYPE_RESOLVER, : TYPE_RESOLVER)

\dropEffectlessElementaries

A variable condition for the taclet construct \dropEffectlessElementaries: given an
update and a target term, it computes the update with all elementary updates removed that do not
affect (are not "relevant" for) the target term, and binds the result to a third schema variable.

Signatures

  • \dropEffectlessElementaries(u: VARIABLE, x: VARIABLE, x2: VARIABLE)

    Instantiates a new drop-effectless-elementaries condition.

    • u the update schema variable whose instantiation is simplified
    • x the schema variable holding the target term which determines the relevant locations
    • x2 the schema variable to which the simplified update application
      u(x) is bound

\dropEffectlessStores

A variable condition for the taclet construct \dropEffectlessStores: it removes all
store operations from a heap term that are overwritten by later store operations
on the same location, and binds the simplified heap to a result schema variable.

Signatures

  • \dropEffectlessStores(h: VARIABLE, o: VARIABLE, f: VARIABLE, x: VARIABLE, result: VARIABLE)

    Instantiates a new drop-effectless-stores condition.

    • h the term schema variable for the heap on which the store operation is performed
    • o the term schema variable for the object of the store operation
    • f the term schema variable for the field of the store operation
    • x the term schema variable for the value written by the store operation
    • result the term schema variable to which the heap with all effectless stores removed
      is bound

\equalUnique

A variable condition for the taclet construct \equalUnique: checks that two term schema
variables are instantiated with (distinct) applications of functions marked as unique,
and if so binds a formula schema variable to the equation of both terms.

Signatures

  • \equalUnique(t: VARIABLE, t2: VARIABLE, res: VARIABLE)

    Instantiates a new equal-unique condition.

    • t the first term schema variable; its instantiation must be the application of a
      unique function
    • t2 the second term schema variable; its instantiation must be the application of a
      unique function
    • res the formula schema variable to which the equation t = t2 is bound

\fieldType

Variable condition that enforces a given generic sort to be instantiated with the type of a field
constant.

The condition can only be fulfilled if the given field term is constant of which the referred
type is known.

Signatures

  • \fieldType(exprOrTypeSV: VARIABLE, sort: SORT)

    Instantiates a new field-type condition.

    • exprOrTypeSV the expression or type schema variable whose instantiation must be a
      field constant
    • sort the generic sort which is instantiated with the referred type of the field

\final

ensures that the given instantiation for the schema variable denotes a final field

Signatures

  • \final(arg0: VARIABLE)

  • \not\final(arg0: VARIABLE)

    the static reference condition checks if a suggested instantiation for a schema variable
    denotes a static reference. The flag negation allows to reuse this condition for ensuring non
    static references.

\freeLabelIn

A variable condition for the taclet construct \freeLabelIn, checking whether a program
label occurs freely in a Java program (block).

Signatures

  • \freeLabelIn(label: VARIABLE, statement: VARIABLE)

  • \not\freeLabelIn(label: VARIABLE, statement: VARIABLE)

    Instantiates a new free-label-in condition.

    • label the schema variable which must be instantiated with a program label
    • statement the schema variable which must be instantiated with a program element
      (statement)
    • negated whether the condition is negated (\not freeLabelIn)

\getFreeInvariant

Extracts the free loop invariants for the given loop term. Free invariants are only assumed, but
not proven (like an axiom).

Signatures

  • \getFreeInvariant(loopStmtSV: VARIABLE, modalitySV: VARIABLE, invSV: VARIABLE)

    Instantiates a new free-loop-invariant condition.

    • loopStmtSV the program schema variable which must be instantiated with a loop
      statement
    • modalitySV the schema variable for the modality kind determining the applicable heap
      context
    • invSV the schema variable to which the free loop invariant is bound

\getInvariant

Extracts the loop invariants for a loop term (for all applicable heap contexts).

Signatures

  • \getInvariant(loopStmtSV: VARIABLE, modalitySV: VARIABLE, invSV: VARIABLE)

    Instantiates a new loop-invariant condition.

    • loopStmtSV the program schema variable which must be instantiated with a loop
      statement
    • modalitySV the schema variable for the modality kind determining the applicable heap
      context
    • invSV the schema variable to which the loop invariant is bound

\getVariant

Extracts the variant for a loop term.

Signatures

  • \getVariant(loopStmtSV: VARIABLE, variantSV: VARIABLE)

    Instantiates a new loop-variant condition.

    • loopStmtSV the program schema variable which must be instantiated with a loop
      statement
    • variantSV the schema variable to which the loop variant is bound

\hasElementarySort

Variable condition that enforces a given generic sort to be instantiated with the sort of a
program expression a schema variable is instantiated with

Signatures

  • \hasElementarySort(: VARIABLE, : SORT)

\hasInvariant

Checks whether a loop has an invariant (either normal or "free").

Signatures

  • \hasInvariant(loopStmtSV: VARIABLE, modalitySV: VARIABLE)

    Instantiates a new has-loop-invariant condition.

    • loopStmtSV the program schema variable which must be instantiated with a loop
      statement
    • modalitySV the schema variable for the modality kind determining the applicable heap
      context

\hasLabel

This variable condition checks if an instantiation for term labels contains a specific term
label.

Signatures

  • \hasLabel(l: VARIABLE, t: STRING)

  • \not\hasLabel(l: VARIABLE, t: STRING)

    Instantiates a new term-label condition.

    • l the term-label schema variable whose instantiations are searched for the label
    • t the name of the term label to look for
    • negated whether the condition is negated (\not hasLabel)

\hasSort

Variable condition that enforces a given generic sort to be instantiated with the sort of a
program expression a schema variable is instantiated with

Signatures

  • \hasSort(: VARIABLE, : SORT)

\hasSubFormulas

This variable condition checks if an instantiation for a formula has sub formulas which are
formulas. It returns false for an arity equal to zero or no sub formulas. This is needed to
simplify distinguishing between different well-definedness operators in taclets, since the
difference exists only for formulas.

Signatures

  • \hasSubFormulas(a: VARIABLE)

  • \not\hasSubFormulas(a: VARIABLE)

    Instantiates a new sub-formula condition.

    • a the formula schema variable whose instantiation is checked for formula sub-terms
    • negated whether the condition is negated (\not hasSubFormulas)

\isAbstractOrInterface

This variable condition checks if a given type denotes an abstract class or interface type.

Signatures

  • \isAbstractOrInterface(tr: TYPE_RESOLVER)

  • \not\isAbstractOrInterface(tr: TYPE_RESOLVER)

    Instantiates a new abstract-or-interface condition.

    • tr the type resolver for the type to check
    • negation whether the condition is negated (\not isAbstractOrInterface)

\isArray

This variable condition checks if an instantiation is an array.

Signatures

  • \isArray(var: VARIABLE)

  • \not\isArray(var: VARIABLE)

    creates an instance of this condition checking if an instantiation of a schema variable is an
    array or not

    • var the SchemaVariable to be checked
    • negated if the result is to be negated upon finding

\isArrayLength

A variable condition for the taclet construct \isArrayLength, checking that a schema
variable is instantiated with the implicit array length attribute.

Signatures

  • \isArrayLength(reference: VARIABLE)

  • \not\isArrayLength(reference: VARIABLE)

    Instantiates a new array length condition.

    • reference the schema variable which must be instantiated with the array length
      attribute of some array type
    • negation whether the condition is negated (\not isArrayLength)

\isConstant

This variable condition checks if an instantiation is a constant formula or term, i.e. its arity
is equal to zero.

Signatures

  • \isConstant(t: VARIABLE)

  • \not\isConstant(t: VARIABLE)

    Instantiates a new constant condition.

    • t the term or formula schema variable which must be instantiated with a constant
      (zero-arity) term
    • negated whether the condition is negated (\not isConstant)

\isEnumConst

ensures that the given instantiation for the schemavariable denotes a constant of an enum type.

Signatures

  • \isEnumConst(arg0: VARIABLE)

    the static reference condition checks if a suggested instantiation for a schema variable
    denotes a reference to an enum constant.

\isFinal

This variable condition checks if a given type denotes a final type.

Final types are either primitive types or classes that are declared final or arrays of final
types.

Signatures

  • \isFinal(tr: TYPE_RESOLVER)

  • \not\isFinal(tr: TYPE_RESOLVER)

    Instantiates a new final-type condition.

    • tr the type resolver for the type to check
    • negation whether the condition is negated (\not isFinal)

\isInStrictFp

This variable condition checks if a context is affected by the strictfp modifier

Signatures

  • \isInStrictFp()

  • \not\isInStrictFp()

    Instantiates a new strict-fp condition.

    • negation whether the condition is negated (\not isInStrictFp)

\isLabeled

Checks whether the given statement is labeled, i.e., actual a LabeledStatement. This information
is obtained from the program prefix.

Signatures

  • \isLabeled(stmtSV: VARIABLE)

  • \not\isLabeled(stmtSV: VARIABLE)

    Instantiates a new is-labeled condition.

    • stmtSV the program schema variable which must be instantiated with a labeled
      statement
    • negated whether the condition is negated (\not isLabeled)

\isLocalVariable

Ensures the given ProgramElement denotes a local variable

Signatures

  • \isLocalVariable(var: VARIABLE)

  • \not\isLocalVariable(var: VARIABLE)

    Instantiates a new local-variable condition.

    • var the schema variable which must be instantiated with a local (non-member) program
      variable
    • neg whether the condition is negated (\not isLocalVariable)

\isModelField

This variable condition checks if the instantiation of a schemavariable (of
type Field) refers to a Java field declared as "model".

The negated condition is true if the instantiation refers to an instance
or static or ghost field.

Signatures

  • \isModelField(field: VARIABLE)

  • \not\isModelField(field: VARIABLE)

    Instantiates a new model-field condition.

    • field the schema variable which must be instantiated with a Java field
    • negated whether the condition is negated (\not isModelField)

\isObserver

A variable condition for the taclet construct \isObserver, checking that a term schema
variable is instantiated with an observer function and that the heap argument of the observer is
the given heap term. If the heap schema variable is not yet instantiated, it is bound to the
observer's heap argument.

Signatures

  • \isObserver(obs: VARIABLE, heap: VARIABLE)

    Instantiates a new observer condition.

    • obs the term schema variable which must be instantiated with an observer function
      application (an {@link IObserverFunction IObserverFunction})
    • heap the term schema variable for the heap on which the observer is evaluated; it is
      automatically instantiated with the observer's heap argument if not yet instantiated

\isReference

This variable condition checks if a schemavariable is instantiated with a reference or primitive
type

Signatures

  • \isReference(: TYPE_RESOLVER)
  • \not\isReference(: TYPE_RESOLVER)

\isReferenceArray

This variable condition checks if an array component is of reference type

Signatures

  • \isReferenceArray(var: VARIABLE)

  • \not\isReferenceArray(var: VARIABLE)

    creates an instance of this condition checking if array var has reference or primitive
    component type depending on the value of checkReferenceType

    • var the SchemaVariable to be checked
    • checkReferenceType the boolean flag which when is set (true) FIXME weigl:
      this is not true! checkReferenceType is just the negated flag. checks for reference
      otherwise for primitive type

\isStaticField

This variable condition checks if the instantiation of a schemavariable (of type Field) refers to
a Java field declared as "static".

The negated condition is true if the instantiation refers to an instance (non-static) field.

Inspired by FieldTypeToSortCondition FieldTypeToSortCondition.

Signatures

  • \isStaticField(field: VARIABLE)

  • \not\isStaticField(field: VARIABLE)

    Instantiates a new static-field condition.

    • field the schema variable which must be instantiated with a Java field
    • negated whether the condition is negated (\not isStaticField)

\isThisReference

This variable condition checks if a program variable is instantiated with a this
reference.

Signatures

  • \isThisReference(var: VARIABLE)

  • \not\isThisReference(var: VARIABLE)

    Instantiates a new this-reference condition.

    • var the program schema variable which must be instantiated with a this
      reference
    • negation whether the condition is negated (\not isThisReference)

\mayExpandMethod

ensures that the given instantiation for the schemavariable denotes a method whose body may be
expanded. For determining the method the callee and the arguments of the method are needed as
arguments.

A method may be inlinded if:

  • the method is private, or
  • the method is static, or
  • the method is final, or
  • the receiver class is final, or
  • the corresponding taclet option is set to relaxed inlining.

Signatures

  • \mayExpandMethod(receiver: VARIABLE, methname: VARIABLE, args: VARIABLE)

  • \not\mayExpandMethod(receiver: VARIABLE, methname: VARIABLE, args: VARIABLE)

    Instantiate a new variable condition.

    • receiver program schema var for the receiver, may be null for class-local calls
    • methname non-null program schema var for the methodname
    • args non-null program schema var for the arguments of the call
    • negation true iff the condition is to be negated
  • \mayExpandMethod(methodName: VARIABLE, args: VARIABLE)

  • \not\mayExpandMethod(methodName: VARIABLE, args: VARIABLE)

    Instantiate a new variable condition for class-local method calls without an explicit
    receiver.

    • methodName non-null program schema var for the methodname
    • args non-null program schema var for the arguments of the call
    • negation true iff the condition is to be negated

\metaDisjoint

A variable condition for the taclet construct \metaDisjoint, checking that the two
instantiations of the given term schema variables denote provably disjoint location sets.

Signatures

  • \metaDisjoint(s1: VARIABLE, s2: VARIABLE)

    Instantiates a new meta-disjoint condition.

    • s1 the first term schema variable; its instantiation is interpreted as a location set
    • s2 the second term schema variable; its instantiation is interpreted as a location set

\new

Signatures

  • \new(arg0: VARIABLE, arg1: JAVA_TYPE)

  • \new(arg0: VARIABLE, arg1: SORT)

\newDependingOn

Signatures

  • \newDependingOn(first: VARIABLE, second: VARIABLE)

\newLabel

This variable condition ensures that no other label of the same name exists in the context
program or one of the schemavariable instantiations.

Signatures

  • \newLabel(sv: VARIABLE)

    Instantiates a new jump-label condition.

    • sv the program schema variable of sort LABEL which must be instantiated with
      a label not occurring elsewhere in the context program or the schema variable
      instantiations

\newLocalVars

For the loop scope rule, if a local program variable that may be altered by the loop body appears
in the frame condition,
it is necessary to use the value before the loop first executes in the frame condition.


To achieve this, this condition generates (1) the "before" version of each variable that may be
written to by the loop
MiscTools#getLocalOuts(ProgramElement, Services) MiscTools#getLocalOuts(ProgramElement, Services); (2) an update storing the value of each
such PV in its "before" version,
i.e., {...||i_before := i||...``; (3) the reverse of the update, to be applied to the frame condition, i.e., {...||i := i_before||...``.

Signatures

  • \newLocalVars(varDeclsSV: VARIABLE, updateBeforeSV: VARIABLE, updateFrameSV: VARIABLE, bodySV: VARIABLE)

    Instantiates a new local-variables condition.

    • varDeclsSV the schema variable to which the variable declarations for the "before"
      versions of the variables written by the loop are bound
    • updateBeforeSV the schema variable to which the update
      {...||i_before := i||...} is bound
    • updateFrameSV the schema variable to which the reverse update
      {...||i := i_before||...} is bound
    • bodySV the schema variable holding the loop body

\newTypeOf

Signatures

  • \newTypeOf(arg0: VARIABLE, arg1: VARIABLE)

\notFreeIn

Signatures

  • \notFreeIn(: VARIABLE, : VARIABLE, : VARIABLE, : VARIABLE, : VARIABLE, : VARIABLE)

  • \not\notFreeIn(: VARIABLE, : VARIABLE, : VARIABLE, : VARIABLE, : VARIABLE, : VARIABLE)

  • \notFreeIn(: VARIABLE, : VARIABLE, : VARIABLE, : VARIABLE, : VARIABLE)

  • \not\notFreeIn(: VARIABLE, : VARIABLE, : VARIABLE, : VARIABLE, : VARIABLE)

  • \notFreeIn(: VARIABLE, : VARIABLE, : VARIABLE, : VARIABLE)

  • \not\notFreeIn(: VARIABLE, : VARIABLE, : VARIABLE, : VARIABLE)

  • \notFreeIn(: VARIABLE, : VARIABLE, : VARIABLE)

  • \not\notFreeIn(: VARIABLE, : VARIABLE, : VARIABLE)

  • \notFreeIn(: VARIABLE, : VARIABLE)

  • \not\notFreeIn(: VARIABLE, : VARIABLE)

\reference

This variable condition checks if a type is an enum type.

Signatures

  • \reference(resolver: VARIABLE)

  • \not\reference(resolver: VARIABLE)

    creates a condition that checks if a type is a EnumDeclaration

    • resolver the type resolver to be checked
    • negated should the result be negated

\same

General varcond for checking relationships between types of schema variables.

Signatures

  • \same(fst: TYPE_RESOLVER, snd: TYPE_RESOLVER)

  • \not\same(fst: TYPE_RESOLVER, snd: TYPE_RESOLVER)

    creates a condition that checks if the declaration types of the schemavariable's
    instantiations are unequal

    • fst one of the SchemaVariable whose type is checked
    • snd one of the SchemaVariable whose type is checked
    • mode an int encoding if testing of not same or not compatible

\sameObserver

A variable condition that is satisfied if the two arguments are

  • schema variables,
  • their instantiations are terms of observer functions,
  • with the same function,
  • which as exactly one heap argument
  • and has got a dependency contract

,

Limitations

Currently, this and de.uka.ilkd.key.rule.metaconstruct.ObserverEqualityMetaConstruct de.uka.ilkd.key.rule.metaconstruct.ObserverEqualityMetaConstruct only
support observers with a single heap argument, that should be generalised.

Signatures

  • \sameObserver(schema1: VARIABLE, schema2: VARIABLE)

    Create a new condition

    • schema1 first argument, must be schema variable
    • schema2 2nd argument, must be schema variable
  • \sameObserver(schema1: VARIABLE, schema2: VARIABLE)

    Create a new condition

    • schema1 first argument, must be schema variable
    • schema2 2nd argument, must be schema variable

\scrictSub

General varcond for checking relationships between types of schema variables.

Signatures

  • \scrictSub(: TYPE_RESOLVER, : TYPE_RESOLVER)

\simplifyIfThenElseUpdate

A variable condition for the taclet construct \simplifyIfThenElseUpdate: it turns two
updates guarded by a condition phi into a single parallel update whose right-hand sides
are if phi then r1 else r2 expressions, and binds the common condition to a formula
schema variable.

Signatures

  • \simplifyIfThenElseUpdate(phi: VARIABLE, u1: VARIABLE, u2: VARIABLE, commonFormula: VARIABLE, result: VARIABLE)

    Instantiates a new simplify-if-then-else-update condition.

    • phi the formula schema variable guarding the two updates
    • u1 the update schema variable applied if phi holds
    • u2 the update schema variable applied if phi does not hold
    • commonFormula the formula schema variable to which the guard phi is bound
    • result the schema variable to which the merged update
      if phi then u1 else u2 is bound
  • \simplifyIfThenElseUpdate(phi: VARIABLE, u1: VARIABLE, u2: VARIABLE, commonFormula: VARIABLE, result: VARIABLE)

    Instantiates a new simplify-if-then-else-update condition.

    • phi the formula schema variable guarding the two updates
    • u1 the update schema variable applied if phi holds
    • u2 the update schema variable applied if phi does not hold
    • commonFormula the formula schema variable to which the guard phi is bound
    • result the schema variable to which the merged update
      if phi then u1 else u2 is bound

\static

ensures that the given instantiation for the schemavariable denotes a static field

Signatures

  • \static(arg0: VARIABLE)

  • \not\static(arg0: VARIABLE)

    the static reference condition checks if a suggested instantiation for a schema variable
    denotes a static reference. The flag negation allows to reuse this condition for ensuring non
    static references.

\staticMethodReference

ensures that the given instantiation for the schemavariable denotes a static method. For
determining the method the callee and the arguments of the method are needed as arguments.

Signatures

  • \staticMethodReference(arg0: VARIABLE, arg1: VARIABLE, arg2: VARIABLE)

  • \not\staticMethodReference(arg0: VARIABLE, arg1: VARIABLE, arg2: VARIABLE)

    the static reference condition checks if a suggested instantiation for a schema variable
    denotes a static method call. The flag negation allows to reuse this condition for ensuring
    non static references.

\storeStmtIn

Stores the given Statement Statement, after substitution of SchemaVariable SchemaVariables, into the given
ProgramSV ProgramSV for later use in other conditions and transformers. The arguments are a
ProgramSV ProgramSV and a JTerm JTerm, where the JTerm JTerm must contain a JavaBlock JavaBlock
with a StatementBlock StatementBlock containing a single statement (that works, e.g., when
passing an expression like
\modality{#allmodal{ while (#e) #body \endmodality(post)); this statement is then
stored (in the example the while statement).

Signatures

  • \storeStmtIn(resultVarSV: VARIABLE, term: TERM)

    Instantiates a new store-statement-in condition.

    • resultVarSV the program schema variable to which the extracted statement is bound
    • term the term whose Java block contains the statement to store; the statement is
      substituted with the current schema variable instantiations first

\storeTermIn

Stores the given JTerm JTerm, after substitution of SchemaVariable SchemaVariables, into the given
SchemaVariable SchemaVariable for later use in other conditions and transformers.

Signatures

  • \storeTermIn(resultVarSV: VARIABLE, term: TERM)

    Instantiates a new store-term-in condition.

    • resultVarSV the schema variable to which the (substituted) term is bound
    • term the term to store; it is substituted with the current schema variable
      instantiations first

\sub

General varcond for checking relationships between types of schema variables.

Signatures

  • \sub(fst: TYPE_RESOLVER, snd: TYPE_RESOLVER)

  • \not\sub(fst: TYPE_RESOLVER, snd: TYPE_RESOLVER)

    creates a condition that checks if the declaration types of the schemavariable's
    instantiations are unequal

    • fst one of the SchemaVariable whose type is checked
    • snd one of the SchemaVariable whose type is checked
    • mode an int encoding if testing of not same or not compatible

\subFormulas

This variable condition checks if an instantiation for a formula has sub formulas which are
formulas. It returns false for an arity equal to zero or no sub formulas. This is needed to
simplify distinguishing between different well-definedness operators in taclets, since the
difference exists only for formulas.

Signatures

  • \subFormulas(a: VARIABLE)

  • \not\subFormulas(a: VARIABLE)

    Instantiates a new sub-formula condition.

    • a the formula schema variable whose instantiation is checked for formula sub-terms
    • negated whether the condition is negated (\not hasSubFormulas)

@wadoon
wadoon enabled auto-merge October 11, 2026 14:51
.replace("<li>", "* ")
.replace("</li>", "")
.replace("{@link", "`")
.replace("}", "`")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This seems a bit radical. What about curly braces in {@code ...}?

And {@link ...} should be ideally translated to [...].

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I also have the heavyweight code of Javadoc parsing, but do you think we should go this here. Or just postpone this to the consumers of the documentation?

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Converts a Javadoc comment into Markdown.
 *
 * <p>Usage:
 * <pre>
 *   java JavadocToMarkdown.java MyClass.java   // converts every Javadoc comment in the file
 *   cat comment.txt | java JavadocToMarkdown.java
 * </pre>
 *
 * <p>Requires Java 11 or newer.
 */
public final class JavadocToMarkdown {

    private JavadocToMarkdown() {}

    // Private-use character that delimits placeholders for protected text.
    private static final char MARK = '\uE000';

    private static final Pattern COMMENT = Pattern.compile("/\\*\\*.*?\\*/", Pattern.DOTALL);
    private static final Pattern BLOCK_TAG = Pattern.compile("^@([A-Za-z][\\w.-]*)\\b\\s*");
    private static final Pattern PRE =
            Pattern.compile("<pre\\b[^>]*>(.*?)</pre\\s*>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
    private static final Pattern PLACEHOLDER = Pattern.compile(MARK + "(\\d+)" + MARK);

    private static final Set<String> HANDLED_TAGS =
            Set.of("param", "return", "throws", "exception", "see", "deprecated",
                   "author", "version", "since");

    // ------------------------------------------------------------------
    // Public API
    // ------------------------------------------------------------------

    /** Converts one Javadoc comment (with or without the delimiters) to Markdown. */
    public static String convert(String javadoc) {
        Parsed parsed = parse(stripComment(javadoc));
        List<String> blocks = new ArrayList<>();

        // Deprecation notice first, as a block quote.
        for (BlockTag t : parsed.tags) {
            if (t.name.equals("deprecated")) {
                String text = "**Deprecated.** " + inline(t.text.toString());
                blocks.add("> " + text.strip().replace("\n", "\n> "));
                break;
            }
        }

        // Main description.
        String description = inline(parsed.description.toString());
        if (!description.isEmpty()) {
            blocks.add(description);
        }

        // @param (type parameters like <T> are listed separately).
        List<String> params = new ArrayList<>();
        List<String> typeParams = new ArrayList<>();
        for (BlockTag t : parsed.tags) {
            if (t.name.equals("param")) {
                String line = namedItem(t.text.toString());
                (t.text.toString().strip().startsWith("<") ? typeParams : params).add(line);
            }
        }
        addList(blocks, "Type parameters", typeParams);
        addList(blocks, "Parameters", params);

        // @return
        for (BlockTag t : parsed.tags) {
            if (t.name.equals("return")) {
                blocks.add("**Returns**\n\n" + inline(t.text.toString()));
                break;
            }
        }

        // @throws / @exception
        List<String> throwsList = new ArrayList<>();
        for (BlockTag t : parsed.tags) {
            if (t.name.equals("throws") || t.name.equals("exception")) {
                throwsList.add(namedItem(t.text.toString()));
            }
        }
        addList(blocks, "Throws", throwsList);

        // @see
        List<String> see = new ArrayList<>();
        for (BlockTag t : parsed.tags) {
            if (t.name.equals("see")) {
                String text = t.text.toString().strip();
                boolean freeform = text.startsWith("<") || text.startsWith("\"");
                see.add("- " + (freeform ? inline(text) : "`" + text.replaceAll("\\s+", " ") + "`"));
            }
        }
        addList(blocks, "See also", see);

        // @since / @version / @author
        for (String name : new String[] {"since", "version", "author"}) {
            for (BlockTag t : parsed.tags) {
                if (t.name.equals(name)) {
                    String label = Character.toUpperCase(name.charAt(0)) + name.substring(1);
                    blocks.add("**" + label + ":** " + inline(t.text.toString()));
                }
            }
        }

        // Anything else (custom or less common tags).
        for (BlockTag t : parsed.tags) {
            if (!HANDLED_TAGS.contains(t.name)) {
                blocks.add("**@" + t.name + "** " + inline(t.text.toString()));
            }
        }

        return String.join("\n\n", blocks) + "\n";
    }

    // ------------------------------------------------------------------
    // Comment parsing
    // ------------------------------------------------------------------

    private static final class BlockTag {
        final String name;
        final StringBuilder text = new StringBuilder();

        BlockTag(String name) {
            this.name = name;
        }
    }

    private static final class Parsed {
        final StringBuilder description = new StringBuilder();
        final List<BlockTag> tags = new ArrayList<>();
    }

    /** Removes the comment delimiters and the leading asterisks of each line. */
    static String stripComment(String comment) {
        String s = comment.strip().replaceFirst("^/\\*+", "").replaceFirst("\\*+/$", "");
        StringBuilder sb = new StringBuilder();
        for (String line : s.split("\\R", -1)) {
            sb.append(line.replaceFirst("^\\s*\\* ?", "")).append('\n');
        }
        return sb.toString();
    }

    /** Splits the comment body into the main description and the block tags. */
    private static Parsed parse(String body) {
        Parsed parsed = new Parsed();
        BlockTag current = null;
        int preDepth = 0;
        int braceDepth = 0;

        for (String line : body.split("\\R", -1)) {
            String trimmed = line.stripLeading();
            Matcher m = BLOCK_TAG.matcher(trimmed);
            // A line starting with @ is a block tag, unless we are inside <pre> or {@...}.
            if (preDepth == 0 && braceDepth == 0 && m.find()) {
                current = new BlockTag(m.group(1));
                parsed.tags.add(current);
                current.text.append(trimmed.substring(m.end()));
            } else if (current != null) {
                current.text.append('\n').append(line);
            } else {
                parsed.description.append(line).append('\n');
            }
            preDepth = Math.max(0, preDepth + count(line, "<pre") - count(line, "</pre"));
            braceDepth = updateBraceDepth(braceDepth, line);
        }
        return parsed;
    }

    private static int count(String haystack, String needle) {
        String h = haystack.toLowerCase();
        int n = 0;
        for (int i = h.indexOf(needle); i >= 0; i = h.indexOf(needle, i + needle.length())) {
            n++;
        }
        return n;
    }

    private static int updateBraceDepth(int depth, String line) {
        for (int i = 0; i < line.length(); i++) {
            char c = line.charAt(i);
            if (c == '{' && (depth > 0 || line.startsWith("{@", i))) {
                depth++;
            } else if (c == '}' && depth > 0) {
                depth--;
            }
        }
        return depth;
    }

    // ------------------------------------------------------------------
    // Rendering helpers
    // ------------------------------------------------------------------

    private static void addList(List<String> blocks, String heading, List<String> items) {
        if (!items.isEmpty()) {
            blocks.add("**" + heading + "**\n\n" + String.join("\n", items));
        }
    }

    /** Renders "name description" as a list item: - `name` — description. */
    private static String namedItem(String raw) {
        String text = raw.strip();
        int ws = indexOfWhitespace(text);
        String name = ws < 0 ? text : text.substring(0, ws);
        String desc = ws < 0 ? "" : text.substring(ws).strip();
        return "- `" + name + "`" + (desc.isEmpty() ? "" : " — " + inline(desc));
    }

    private static int indexOfWhitespace(String s) {
        for (int i = 0; i < s.length(); i++) {
            if (Character.isWhitespace(s.charAt(i))) {
                return i;
            }
        }
        return -1;
    }

    // ------------------------------------------------------------------
    // Inline conversion: {@tags}, HTML, <pre> blocks
    // ------------------------------------------------------------------

    /** Converts running Javadoc text (inline tags and HTML) to Markdown. */
    private static String inline(String text) {
        List<String> store = new ArrayList<>();

        String s = extractPre(text, store);
        s = expandInline(s, false, store);

        // Javadoc line breaks are just whitespace, so normalise each line.
        String[] lines = s.split("\\R", -1);
        for (int i = 0; i < lines.length; i++) {
            lines[i] = lines[i].strip();
        }
        s = String.join("\n", lines);

        s = convertHtml(s);
        s = s.replaceAll("\n{3,}", "\n\n").strip();
        return restore(s, store);
    }

    /** Replaces &lt;pre&gt; blocks with fenced code blocks (stored as placeholders). */
    private static String extractPre(String text, List<String> store) {
        Matcher m = PRE.matcher(text);
        StringBuilder sb = new StringBuilder();
        while (m.find()) {
            String code = expandInline(m.group(1), true, store);
            code = code.replaceAll("(?i)</?(code|tt)\\s*>", "");
            code = unescapeHtml(code);
            String fenced = "```java\n" + tidyCode(code) + "\n```";
            m.appendReplacement(sb, Matcher.quoteReplacement("\n\n" + protect(fenced, store) + "\n\n"));
        }
        m.appendTail(sb);
        return sb.toString();
    }

    /**
     * Expands {@code {@tag ...}} constructs (with balanced braces).
     *
     * @param raw true inside a code block, where no Markdown decoration is added
     */
    private static String expandInline(String s, boolean raw, List<String> store) {
        StringBuilder out = new StringBuilder();
        int i = 0;
        while (i < s.length()) {
            int start = s.indexOf("{@", i);
            if (start < 0) {
                out.append(s, i, s.length());
                break;
            }
            out.append(s, i, start);
            int end = findClosingBrace(s, start);
            if (end < 0) { // unbalanced: leave as-is
                out.append(s.substring(start));
                break;
            }
            String inner = s.substring(start + 2, end);
            int ws = indexOfWhitespace(inner);
            String tag = ws < 0 ? inner : inner.substring(0, ws);
            String arg = ws < 0 ? "" : inner.substring(ws + 1);
            out.append(renderInlineTag(tag, arg, raw, store));
            i = end + 1;
        }
        return out.toString();
    }

    private static int findClosingBrace(String s, int open) {
        int depth = 0;
        for (int i = open; i < s.length(); i++) {
            char c = s.charAt(i);
            if (c == '{') {
                depth++;
            } else if (c == '}' && --depth == 0) {
                return i;
            }
        }
        return -1;
    }

    private static String renderInlineTag(String tag, String arg, boolean raw, List<String> store) {
        switch (tag) {
            case "code":
                return raw ? arg : codeSpan(arg.replaceAll("\\s*\\R\\s*", " ").strip(), store);
            case "literal":
                return raw ? arg : protect(arg.replaceAll("\\s*\\R\\s*", " ").strip(), store);
            case "link":
            case "linkplain": {
                String[] parts = splitReference(arg.strip());
                String display = parts[1].isEmpty() ? parts[0] : parts[1];
                if (raw) {
                    return display;
                }
                return tag.equals("link") ? codeSpan(display, store) : protect(display, store);
            }
            case "value":
            case "systemProperty":
                return raw ? arg.strip() : codeSpan(arg.strip(), store);
            case "inheritDoc":
                return raw ? "" : "_(inherited documentation)_";
            case "docRoot":
                return "";
            case "index": {
                String term = arg.strip();
                if (term.startsWith("\"")) {
                    int close = term.indexOf('"', 1);
                    term = close > 0 ? term.substring(1, close) : term.substring(1);
                } else {
                    int ws = indexOfWhitespace(term);
                    term = ws < 0 ? term : term.substring(0, ws);
                }
                return protect(term, store);
            }
            default: // summary, return, snippet, unknown tags: keep the content
                return expandInline(arg.strip(), raw, store);
        }
    }

    /** Splits "Foo#bar(int, String) label" into reference and label. */
    private static String[] splitReference(String arg) {
        int paren = 0;
        for (int i = 0; i < arg.length(); i++) {
            char c = arg.charAt(i);
            if (c == '(') {
                paren++;
            } else if (c == ')') {
                paren--;
            } else if (paren == 0 && Character.isWhitespace(c)) {
                return new String[] {arg.substring(0, i), arg.substring(i).strip()};
            }
        }
        return new String[] {arg, ""};
    }

    private static String codeSpan(String text, List<String> store) {
        if (text.isEmpty()) {
            return "";
        }
        int longest = 0;
        int run = 0;
        for (char c : text.toCharArray()) {
            run = (c == '`') ? run + 1 : 0;
            longest = Math.max(longest, run);
        }
        String fence = "`".repeat(longest + 1);
        String pad = (text.startsWith("`") || text.endsWith("`")) ? " " : "";
        return protect(fence + pad + text + pad + fence, store);
    }

    // ------------------------------------------------------------------
    // HTML -> Markdown
    // ------------------------------------------------------------------

    private static String convertHtml(String s) {
        // Links (and drop empty named anchors).
        s = s.replaceAll("(?is)<a\\s+(?:name|id)\\s*=[^>]*>\\s*</a\\s*>", "");
        s = s.replaceAll("(?is)<a\\s+[^>]*?href\\s*=\\s*[\"']([^\"']*)[\"'][^>]*>(.*?)</a\\s*>", "[$2]($1)");

        // Headings.
        s = Pattern.compile("(?is)<h([1-6])\\b[^>]*>(.*?)</h\\1\\s*>").matcher(s).replaceAll(r ->
                Matcher.quoteReplacement("\n\n" + "#".repeat(Integer.parseInt(r.group(1)))
                        + " " + r.group(2).strip() + "\n\n"));

        // Lists.
        s = convertLists(s);

        // Inline code: if it already contains protected spans, just drop the tags.
        s = Pattern.compile("(?is)<(code|tt)\\b[^>]*>(.*?)</\\1\\s*>").matcher(s).replaceAll(r ->
                Matcher.quoteReplacement(r.group(2).indexOf(MARK) >= 0 ? r.group(2) : "`" + r.group(2) + "`"));

        // Simple tags.
        s = s.replaceAll("(?i)<p\\b[^>]*>|</p\\s*>", "\n\n");
        s = s.replaceAll("(?i)<br\\s*/?>", "  \n");
        s = s.replaceAll("(?i)<hr\\s*/?>", "\n\n---\n\n");
        s = s.replaceAll("(?i)</?(b|strong)\\s*>", "**");
        s = s.replaceAll("(?i)</?(i|em)\\s*>", "*");

        // Entities that read better decoded (&lt; &gt; &amp; are valid Markdown as-is).
        return s.replace("&#64;", "@").replace("&#x40;", "@").replace("&nbsp;", " ");
    }

    /** Converts ul/ol/li into Markdown lists, including nesting. */
    private static String convertLists(String s) {
        Matcher m = Pattern.compile("(?i)<(/?)(ul|ol|li)\\b[^>]*>").matcher(s);
        StringBuilder out = new StringBuilder();
        Deque<int[]> stack = new ArrayDeque<>(); // {ordered ? 1 : 0, item counter}
        int last = 0;
        while (m.find()) {
            out.append(s, last, m.start());
            last = m.end();
            boolean closing = !m.group(1).isEmpty();
            String name = m.group(2).toLowerCase();
            if (name.equals("li")) {
                if (!closing && !stack.isEmpty()) {
                    int[] top = stack.peek();
                    if (top[1]++ > 0) { // not the first item: keep the list tight
                        while (out.length() > 0 && Character.isWhitespace(out.charAt(out.length() - 1))) {
                            out.setLength(out.length() - 1);
                        }
                    }
                    String bullet = top[0] == 1 ? top[1] + "." : "-";
                    out.append('\n').append("  ".repeat(stack.size() - 1)).append(bullet).append(' ');
                }
            } else if (!closing) {
                if (stack.isEmpty()) {
                    out.append('\n');
                }
                stack.push(new int[] {name.equals("ol") ? 1 : 0, 0});
            } else {
                if (!stack.isEmpty()) {
                    stack.pop();
                }
                out.append('\n');
                if (stack.isEmpty()) {
                    out.append('\n');
                }
            }
        }
        return out.append(s.substring(last)).toString();
    }

    private static String unescapeHtml(String s) {
        return s.replace("&lt;", "<").replace("&gt;", ">").replace("&quot;", "\"")
                .replace("&nbsp;", " ").replace("&#64;", "@").replace("&#x40;", "@")
                .replace("&amp;", "&");
    }

    /** Trims blank lines around code and removes common indentation. */
    private static String tidyCode(String code) {
        List<String> lines = new ArrayList<>(Arrays.asList(code.split("\\R", -1)));
        while (!lines.isEmpty() && lines.get(0).isBlank()) {
            lines.remove(0);
        }
        while (!lines.isEmpty() && lines.get(lines.size() - 1).isBlank()) {
            lines.remove(lines.size() - 1);
        }
        int indent = Integer.MAX_VALUE;
        for (String l : lines) {
            if (!l.isBlank()) {
                int n = 0;
                while (n < l.length() && l.charAt(n) == ' ') {
                    n++;
                }
                indent = Math.min(indent, n);
            }
        }
        if (indent == Integer.MAX_VALUE) {
            indent = 0;
        }
        List<String> result = new ArrayList<>();
        for (String l : lines) {
            result.add(l.isBlank() ? "" : l.substring(Math.min(indent, l.length())).stripTrailing());
        }
        return String.join("\n", result);
    }

    // ------------------------------------------------------------------
    // Placeholders: protect finished Markdown from later HTML processing
    // ------------------------------------------------------------------

    private static String protect(String text, List<String> store) {
        store.add(text);
        return MARK + String.valueOf(store.size() - 1) + MARK;
    }

    private static String restore(String s, List<String> store) {
        return PLACEHOLDER.matcher(s).replaceAll(r -> Matcher.quoteReplacement(store.get(Integer.parseInt(r.group(1)))));
    }

    // ------------------------------------------------------------------
    // Command line
    // ------------------------------------------------------------------

    public static void main(String[] args) throws IOException {
        String input = args.length > 0
                ? Files.readString(Path.of(args[0]), StandardCharsets.UTF_8)
                : new String(System.in.readAllBytes(), StandardCharsets.UTF_8);

        List<String> results = new ArrayList<>();
        Matcher m = COMMENT.matcher(input);
        while (m.find()) {
            results.add(convert(m.group()));
        }
        if (results.isEmpty() && !input.isBlank()) { // plain text without delimiters
            results.add(convert(input));
        }
        System.out.print(String.join("\n---\n\n", results));
    }
}

@wadoon wadoon mentioned this pull request Oct 22, 2026
2 tasks done
Determining whether a varcond implementation supports negation no longer
matches the declared ArgumentType classes against the constructor's
parameter types. The argument types are only coarse categories used for
parsing and do not match the concrete parameter types (e.g. \isConstant
expects an ArgumentType.VARIABLE, but ConstantCondition's constructor
takes a JAbstractSortedOperator). A failed match made isNegationSupported
throw an IllegalStateException, which was swallowed by TacletPBuilder's
manipulator application, so the varcond could not be applied at all and
taclet files using \isConstant or \hasLabel failed to load, breaking
all key.core.wd run-all-proofs tests.

The negation support check is restored to the previous lenient behavior:
the first declared public constructor's last parameter must be boolean.

Constructor lookup for documentation purposes now matches by parameter
count and falls back to empty javadoc if the class was not processed by
the therapi scribe. Also fix the warning in TacletPBuilder to print the
argument types array instead of its identity.
Reintroduces the check that a varcond implementation constructor matches
the declared ArgumentTypes, which was dropped as part of the initial fix
for the wd proof tests.

The negation-support detection is now tiered: a constructor whose
parameter types are compatible with the declared argument types is
preferred and decides negation support by its trailing boolean
parameter. Only if no type-compatible constructor exists (the
ArgumentType categories only approximate the real parameter types, as
for \isConstant and \hasLabel), the previous lenient fallback on the
first public constructor is retained. findConstructor (used for
documentation lookup) prefers a type-compatible constructor and falls
back to matching by parameter count.

Verified: :key.core.wd:testRunAllWdProofs passes 9/9.
The documentation generators (VarcondDoc, ScriptDoc) extract Javadocs
via therapi from de.uka.ilkd.key.rule.conditions and
de.uka.ilkd.key.scripts. Complete the gaps so the generated
documentation is not empty:

- rule.conditions: add missing class documentation (ArrayLength,
  DifferentInstantiation, DropEffectlessElementaries,
  DropEffectlessStores, EqualUnique, FreeLabelIn, MetaDisjoint,
  Observer, SimplifyIfThenElseUpdate) and constructor documentation
  with @PARAM descriptions for all variable condition implementations
  and TypeResolver's nested resolvers; fix the copy-pasted class
  documentation of IsThisReference.
- scripts: document TermWithHoles and SequentWithHoles, fill the empty
  class documentation of the deprecated SchemaVarCommand, and add the
  missing class-level @documentation to the Parameters classes of
  branches, expandDef, rewrite, and set.
- normalize license headers to the spotless template (ScriptDoc,
  VarcondDoc, TacletBuilderCommandInfo).
…build

Add the Gradle tasks generateVarcondDoc and generateScriptDoc, which run
the documentation generators (VarcondDoc, ScriptDoc) and write the
resulting markdown to key.core/build/docs/keydocs.

The nightly workflow additionally sets up JDK 25 for the generation step
(therapi's scribe only extracts '///' markdown javadocs on JDK 23+),
packages the generated files and uploads them as the artifact
"varcond-script-docs" with a retention of 30 days.
- ArgumentsLifter: null-guard the therapi javadoc lookups, which return
  null for classes compiled with a JDK that does not recognize the used
  doc comment style, and format field javadoc comments with the
  CommentFormatter instead of printing the raw FieldJavadoc object.
- TacletBuilderCommandInfoImpl: bound the @PARAM name lookup by the
  documented parameter count, which may be shorter than the declared
  argument types.
…riptDoc

Extract the common functionality of the documentation generators into the
new base class AbstractDocGenerator:

- output handling: the generators write to stdout, or to the file given
  as the first command line argument (creating missing directories),
  which simplifies the Gradle tasks generateVarcondDoc/generateScriptDoc
- javadoc post-processing: cleanJavadoc and indent are moved here and
  are now also applied by ScriptDoc
- ScriptDoc additionally prints a section header per command, matching
  the format of VarcondDoc
The generators now also emit a structured JSON dump next to the markdown
file (varconds.json, script-commands.json), containing the same
information in a machine-readable form: names, categories, deprecation
flags, descriptions, signatures with negation support, and arguments
with kind, type, position and description.

The shared base class AbstractDocGenerator gains a generateJsonData()
hook; the new dependency-free JsonWriter serializes the data. The
nightly workflow's packaging step picks the JSON files up unchanged.
- Annotate all program and term transformers with @TransformerMeta(name=...),
  using the internal meta-construct name passed as the first argument to
  super(..); mark the annotation @retention(RUNTIME) and read it from the new
  CodeTransformerDoc generator.
- Add CodeTransformerDoc producing markdown + JSON dumps of all code
  transformers, register the generateCodeTransformerDoc Gradle task and run it
  in the nightly build (nightlydeploy.yml).
- Rework AbstractDocGenerator.cleanJavadoc: parse inline tags with a
  brace-balanced scanner (preserving braces inside {code} and {snippet}),
  translate {link ...} to markdown links and {snippet : ...} to fenced code
  blocks; fix HTML entity/tag conversion.
- Extend -Ajavadoc.packages to de.uka.ilkd.key.rule.metaconstruct.
The documentation generators (VarcondDoc, ScriptDoc, CodeTransformerDoc)
previously ran two parallel traversals to produce markdown and JSON, which
kept drifting apart (e.g. transformer parameters were javadoc names in the
markdown but reflection names arg0/arg1 in the JSON).

Generators now build a common data model once - List<DocItem> with
DocSignature and DocParameter - that is both dumped verbatim as JSON and
rendered to markdown by the shared AbstractDocGenerator base class. The
records cover all three documentation types: variable conditions (overloads,
negatable signatures), proof script commands (category, deprecated, argument
kinds/position/varargs) and code transformers (constructors). JsonWriter
serializes records via their record components. Markdown output additionally
uses javadoc parameter names and shows a signatures section for script
commands; all text is cleanJavadoc-processed in both outputs.

Verification: compileJava and spotlessApply clean; generateVarcondDoc,
generateScriptDoc and generateCodeTransformerDoc produce valid JSON with
unchanged item counts (53 varconds, 39 script commands, 52 transformers).
@wadoon

wadoon commented Oct 22, 2026

Copy link
Copy Markdown
Member Author

@unp1 I also added the program/term transformer, but the documentation there is also rather poor:


#addCast

Signatures

  • #addCast()

#arrayBaseInstanceOf

Creates an instance<Type>(..) term for the component type of the array. The component
type has to be a reference type.

Signatures

  • #arrayBaseInstanceOf()

#length-reference

Signatures

  • #length-reference(expr: de.uka.ilkd.key.java.ast.expression.Expression)

    creates a typeof ProgramTransformer

    • expr the instance of expression contained by the meta construct

array-post-declaration

Replaces a local variable declaration #t #v[]; with #t[] #v;

Signatures

  • array-post-declaration(arg0: org.key_project.logic.op.sv.SchemaVariable)

#constantvalue

Replace a program variable that is a compile-time constant with the value of the initializer

Signatures

  • #constantvalue()

constructor-call

The constructor call meta construct is used to handle a allocation expression like
new Class(...). Thereby it replaces the allocation expression by a method reference
to an implict method called <init> that is mainly the constructor but in its
normalform.

Signatures

  • constructor-call(arg0: de.uka.ilkd.key.logic.op.ProgramVariable, arg1: de.uka.ilkd.key.java.ast.expression.operator.New)

    Used to programmatically produce this statement. There is no schema variable instantiation,
    since we have a concrete variable.

  • constructor-call(newObjectSV: org.key_project.logic.op.sv.SchemaVariable, consRef: de.uka.ilkd.key.java.ast.ProgramElement)

    creates the metaconstruct

    • newObjectSV TODO
    • consRef TODO

#createBeforeLoopUpdate

Initializes the "before loop" update needed for the modifiable clause.

Only remembers the heaps of the context, not the changed local variables, although this is done
for the built-in loop invariant rules, where they however are never used.

NOTE: If the local variables should be remembered, we have to find a way to declare them globally
in taclets, which currently is not possible for statically unknown lists of variables. Variable
conditions cannot access the goal-local namespaces, only the global one.

Expects as arguments the loop formula (for determining the relevant heap contexts) and three
Skolem terms for the currently implemented heaps: The normal heap, the savedHeap for
transactions, and the permissions heap.

Signatures

  • #createBeforeLoopUpdate()

#createFrameCond

Creates the frame condition (aka "modifiable clause") for the given loop. Also accepts the
pre-state update and extracts the symbols from there. New symbols in the pre-state update (like
"heap_BeforeLOOP") are added to the namespaces. This is because the update is, for the loop scope
invariant taclet, created by a variable condition; new symbols created there are not
automatically stored in the proof, or will be generated/stored multiple times.

Signatures

  • #createFrameCond()

#createHeapAnonUpdate

Creates the anonymizing update for the heap. Expects as arguments the loop formula (for
determining the relevant heap contexts) and three Skolem terms for the currently implemented
heaps: The normal heap, the savedHeap for transactions, and the permissions heap.

Signatures

  • #createHeapAnonUpdate()

#createLocalAnonUpdate

Expects a loop body and creates the anonymizing update
out_1:=anon_1||...||out_n:=anon_n, where anon_1, ..., anon_n are the written
variables in the loop body visible to the outside.

Signatures

  • #createLocalAnonUpdate()

create-object

If an allocation expression new Class(...) occurs, a new object has to be created,
in KeY this is quite similar to take it out of a list of objects and setting the implicit flag
<created> to true as well as setting all fields of the object
to their default values. For the complete procedure, the method creates the implicit method
<createObject> which on its part calls another implicit method
lt;prepare> for setting the fields values.

Signatures

  • create-object(newExpr: de.uka.ilkd.key.java.ast.ProgramElement)

#wellFormedCond

Creates the wellformedness condition for the given anonymizing heap terms if they apply for the
current profile and modality type. At least generates the "wellFormed(anon_heap_LOOP)" condition
for the anonymized standard heap.

Signatures

  • #wellFormedCond()

do-break

This class performs a labeled break. This means l1:l2:{l3:{l4:{break l3;}} ...} becomes l1:l2:{...}

Signatures

  • do-break(labeledBreak: de.uka.ilkd.key.java.ast.statement.LabeledStatement)

    creates a do-break ProgramTransformer

    • labeledBreak the LabeledStatement contained by the meta construct

enhancedfor-elim

This class defines a meta operator to resolve an enhanced for loop - by transformation to a
"normal" loop.

This class is used to transform an enh. for loop over an iterable object into a while loop +
surrounding statements.

Signatures

  • enhancedfor-elim(execContextSV: de.uka.ilkd.key.logic.op.ProgramSV, forStatement: de.uka.ilkd.key.java.ast.statement.EnhancedFor)

    Creates a new enhanced for-loop elimination.

    • execContextSV the execution context.
    • forStatement the enhanced for loop to eliminate.

#enumconstantvalue

resolve a program variable to an integer literal.

If the PV is a enum constant, its index in the enum constant array is returned. If the PC is a
reference to the nextToCreate field than the number of enum constants is returned.

Signatures

  • #enumconstantvalue()

#evaluate-arguments

TODO

Signatures

  • #evaluate-arguments(pe: de.uka.ilkd.key.java.ast.ProgramElement)

    creates a typeof ProgramTransformer

    • pe the instance of expression contained by the meta construct

expand-method-body

Replaces the MethodBodyStatement shortcut with the full body, performs prefix adjustments in the
body (execution context).

Signatures

  • expand-method-body(arg0: org.key_project.logic.op.sv.SchemaVariable)

  • expand-method-body(arg0: de.uka.ilkd.key.java.ast.Statement)

#ExpandQueries

Uses the class QueryExpand in order to insert query expansions in the term that the
meta construct is applied on.

Signatures

  • #ExpandQueries()

forInitUnfoldTransformer

Pulls the initializor out of a for-loop. Only receives the init as a parameter, not the whole
for-loop.

Example:

\pi for (init; guard; upd) body \omega

to:

\pi { init' for(; guard; upd) body } \omega

Signatures

  • forInitUnfoldTransformer(loopInit: de.uka.ilkd.key.java.ast.statement.LoopInit)

    • loopInit A loop initializer if called for a concrete program.
  • forInitUnfoldTransformer(programSV: de.uka.ilkd.key.logic.op.ProgramSV)

    • programSV A ProgramSV if called while parsing a taclet.

#for-to-while

converts a for-loop to a while loop. Invariant and other rules cannot be performed on for but
only on while loops.

It makes uses of the ForToWhileTransformation to create a transformed loop body which is
then put into the corresponding context.

Example

 for (int i = 0; i < 100; i++) {
     if (i == 2)
         continue;
     if (i == 3)
         break;
 }
 

is translated to

 _label1: {
     int i = 0;
     while (i < 100) {
         _label0: {
             if (i == 2)
                 break _label0;
             if (i == 3)
                 break _label1;
         }
         i++;
     }
 }
 

Signatures

  • #for-to-while(loop: org.key_project.logic.op.sv.SchemaVariable, innerLabel: org.key_project.logic.op.sv.SchemaVariable, outerLabel: de.uka.ilkd.key.java.ast.Statement)

    creates an loop to while - ProgramTransformer

    • loop the LoopStatement contained by the meta construct
    • innerLabel the label used to handle continue
    • outerLabel the label used to handle break (only needed for do-while-loops)

init-array-creation

Split an array creation expression with explicit array initializer, creating a creation
expression with dimension expression and a list of assignments (-> Java language specification,
15.10)

This meta construct delivers the creation expression

Signatures

  • init-array-creation(arg0: org.key_project.logic.op.sv.SchemaVariable, arg1: de.uka.ilkd.key.java.ast.ProgramElement)

#introAtPreDefs

Transformer that introduces concrete prestate variables

Signatures

  • #introAtPreDefs()

#isstatic

Creates a true or false literal if the given program element is or is not a static variable
reference.

Signatures

  • #isstatic(pe: de.uka.ilkd.key.java.ast.ProgramElement)

    creates a typeof ProgramTransformer

    • pe the instance of expression contained by the meta construct

#memberPVToField

Signatures

  • #memberPVToField()

method-call

Symbolically executes a method invocation.

This class is intentionally final. Its #transform runs on a fresh per-call copy
(see there) so that the instance shared via the taclet base is never mutated and concurrent
method-call expansions in the parallel prover do not race on the scratch instance fields. That
delegation copies this by constructing a plain MethodCall; if a subclass existed
and overrode any of the transform helpers, the copy would silently drop the override and run the
wrong transformation. Forbidding subclasses keeps the copy faithful and the thread-safety
argument sound. Behaviour is varied through the taclet/ProgramElement arguments, not by
subclassing, so nothing is lost.

Signatures

  • method-call(result: de.uka.ilkd.key.logic.op.ProgramSV, body: org.key_project.logic.op.sv.SchemaVariable, arg2: de.uka.ilkd.key.java.ast.ProgramElement)

    creates the methodcall-MetaConstruct

    • result the SchemaVariable that is used to keep the result
    • body the ProgramElement contained by the meta construct
  • method-call(result: org.key_project.logic.op.sv.SchemaVariable, body: de.uka.ilkd.key.java.ast.ProgramElement)

    creates the methodcall-MetaConstruct

    • result the SchemaVariable that is used to keep the result
    • body the ProgramElement contained by the meta construct
  • method-call(body: de.uka.ilkd.key.java.ast.ProgramElement)

    creates the methodcall-MetaConstruct

    • body the ProgramElement contained by the meta construct

multiple-var-decl

Replaces a declaration of multiple variables by two variable declarations where the first one
declares a single variable and the second one the remaining variables.

Signatures

  • multiple-var-decl(arg0: org.key_project.logic.op.sv.SchemaVariable)

#ObserverEquality

This meta contruct allows one to prove equality (equivalence) of two observer terms if they
belong to the same observer function symbol.

It takes two arguments of any sort and the result is a formula.

This construct is asymmetric. The second argument is the call with the base heap, i.e., the one
with fewer created objects. The first argument is the one with the larger heap, i.e., after
modification and with potentially more created objects.

The construct implements the condition (9.10) in Sect. 9.4.4 in the new KeY book.

It is probably less efficient than UseDependencyContractRule, however it is more
generally applicable. It mitigates the problem, that in interactive proving the above builtin
rule is often not available if desired.

Limitation

Currently it works only for observable functions that depend on a single heap. If applied to more
than one heap than the secondary heaps must be equal and are not treated using dependency
contracts.

Signatures

post-work

creates an assignment instantiationOf(#newObjectsV).$initialized = true

Signatures

  • post-work(arg0: org.key_project.logic.op.sv.SchemaVariable)

  • post-work(pv: de.uka.ilkd.key.logic.op.ProgramVariable)

    Used to create this Java statement programmatically. Do not use in taclet meta constructs!

#reattachLoopInvariant

Construct for re-attaching a loop invariant that otherwise would get lost after a transformation,
for instance, the loop scope-based unwinding rule. Copied from the 2015 TimSort tweaks branch (by
DS), original work by Richard. Changed extraction of active statement to also work with labeled
loops.

Signatures

  • #reattachLoopInvariant(arg0: de.uka.ilkd.key.java.ast.statement.LoopStatement)

special-constructor-call

The constructor call meta construct is used to handle a allocation expression like
new Class(...). Thereby it replaces the allocation expression by a method reference
to an implict method called <init> that is mainly the constructor but in its
normalform.

Signatures

  • special-constructor-call(consRef: de.uka.ilkd.key.java.ast.ProgramElement)

    • consRef The constructor reference.

static-initialisation

Signatures

  • static-initialisation(arg0: de.uka.ilkd.key.java.ast.expression.Expression)

switch-to-if

This class is used to perform program transformations needed for the symbolic execution of a
switch-case statement.

Signatures

  • switch-to-if(_switch: org.key_project.logic.op.sv.SchemaVariable)

    creates a switch-to-if ProgramTransformer

    • _switch the Statement contained by the meta construct

#typeof

Signatures

  • #typeof(pe: de.uka.ilkd.key.java.ast.ProgramElement)

    creates a typeof ProgramTransformer

    • pe the instance of expression contained by the meta construct

unpack

Signatures

  • unpack(loop: de.uka.ilkd.key.java.ast.statement.For)

    creates a typeof ProgramTransformer

    • loop the instance of expression contained by the meta construct

#unwind-loop

This class is used to perform program transformations needed for the symbolic execution of a
loop. It unwinds the loop: e.g.

 `
 while ( i<10 ) {
   i++
 `
 }
 

becomes

 `
 if (i < 10)
     l1: {
         l2: {
             i++;
         `
         while (i < 10) { i++; }
     }
 }
 

Signatures

  • #unwind-loop(loop: org.key_project.logic.op.sv.SchemaVariable, innerLabel: org.key_project.logic.op.sv.SchemaVariable, outerLabel: de.uka.ilkd.key.java.ast.statement.LoopStatement)

    creates an unwind-loop ProgramTransformer

    • loop the LoopStatement contained by the meta construct
    • innerLabel The inner label SV
    • outerLabel The outer label SV

#divideLCRMonomials

Metaoperator for computing the result of dividing one monomial by another

Signatures

  • #divideLCRMonomials()

#divideMonomials

Metaoperator for computing the result of dividing one monomial by another

Signatures

  • #divideMonomials()

#add

Signatures

  • #add()

#BinaryAnd

Signatures

  • #BinaryAnd()

#BinaryOr

Signatures

  • #BinaryOr()

#BinaryXOr

Signatures

  • #BinaryXOr()

#div

Signatures

  • #div()

#eq

Signatures

  • #eq()

#geq

Signatures

  • #geq()

#greater

Signatures

  • #greater()

#leq

Signatures

  • #leq()

#less

Signatures

  • #less()

#mul

Signatures

  • #mul()

#pow

Computes the pow function for literals. Computation fails if second argument is negative or
exceeds Integer.MAX_VALUE (the latter due to restrictions of class BigInteger).

Signatures

  • #pow()

#ShiftLeft

Signatures

  • #ShiftLeft()

#ShiftRight

Signatures

  • #ShiftRight()

    creates the transformer for performing a shift to the right

#sub

Signatures

  • #sub()

@wadoon
wadoon requested a review from unp1 October 22, 2026 20:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Documentation Improvements or additions to documentation Java Pull requests that update Java code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants