Writing pf2 Annotations

Proof annotations live in your documentation comments using the prove:pf2 marker. This page teaches the annotation format end to end: how the kernel uses an annotation for soundness, the block structure, where the annotation goes in each language, how to handle code the kernel cannot translate, and how caching keeps re-verification fast.

After this page you will be able to write a complete proof annotation on a real function, choose the right blocks for what you are proving, place the annotation correctly for your language, and know exactly what to do when a function touches something the kernel cannot reason about directly.

How the Kernel Uses an Annotation

An annotation is the mechanism that ties a formal proof to the code it describes. Verification works in three steps:

  1. The code is translated to a formal representation. The language parser reads the annotated code and produces a representation of what it does in the kernel's term language, so the proof cannot claim behavior the code does not have.
  2. The kernel reads the annotation. From the documentation comment it extracts the theorem name, the statement, and the proof script (structured steps that reduce to primitive inference rules).
  3. The kernel checks the proof against the code. The proof must be consistent with the formal representation. A proof cannot lie about what the code does: if it claims a property that contradicts the code, the kernel rejects it.

This grounds every verified annotation in the actual implementation, not just the author's intent. Where a construct cannot be translated (foreign calls, inline assembly, hardware access, reflection), you record the boundary as an explicit cited assumption, described under Documenting External Dependencies below, so what is assumed versus proved is always visible.

Two inputs feed the HOL kernel. Your code passes through a parser to become a formal representation of what it does; your proof annotation becomes a formal statement plus a proof script. The kernel returns Verified only if the proof agrees with both the mathematical foundation and the code representation, otherwise Failed with a precise reason. A side note explains the external boundary: code that cannot be translated becomes a clearly cited assumption rather than a silent gap.
A proof is checked against both the mathematical foundation and a faithful representation of your actual code. Anything untranslatable becomes a visible, cited assumption.

The prove:pf2 Marker

Every proof annotation begins with the marker prove:pf2("theorem_name") inside a documentation comment. The theorem name uniquely identifies the proof within your project. The marker tells the scanner that the comment contains a pf2 proof to verify.

/* prove:pf2("my_theorem_name")
\[
  ... proof body ...
\]
*/

The \[ and \] delimiters wrap the LaTeX proof content. Everything between them is parsed as structured proof notation.

Annotation Shape

Every annotation contains these core elements:

  • Theorem name: unique within the project, declared in the prove:pf2("name") marker
  • Statement: expressed in the kernel’s term language, declaring what the proof establishes
  • Proof script: structured steps that reduce to primitive rules the kernel can check

Optionally, annotations can also include:

  • Domain and category: for organizing proofs in the dashboard
  • Dependencies: references to other theorems this proof builds on
  • Pre/post conditions: formal contracts the function must satisfy

LaTeX Sections

A range of structured block types can appear inside the \[ ... \] delimiters. Only the proof block is required; all others are optional or recommended. The kernel parses the proof block and skips over any other \begin{...} environment up to its matching \end{...}, so the section vocabulary is open-ended. The most common sections are listed below.

Section Purpose Required?
\begin{doc} Human-readable narrative explaining the theorem Optional
\begin{sig} Type signature of the function Recommended
\begin{params} Function parameters and their types Optional
\begin{returns} Return type and description Optional
\begin{pre} Preconditions (HOL boolean expressions) Optional
\begin{post} Postconditions (HOL boolean expressions) Optional
\begin{throws} Exception and error conditions Optional
\begin{fields} Struct or class fields Optional
\begin{columns} SQL table columns Optional
\begin{variants} Enum variants or union cases Optional
\begin{methods} Trait or interface methods Optional
\begin{laws} Algebraic laws the type must satisfy Optional
\begin{invariant} Loop, struct, or module invariant Optional
\begin{establishes} Constructor guarantees Optional
\begin{releases} Destructor resource cleanup guarantees Optional
\begin{sideeffects} Observable state changes Optional
\begin{proof} Proof steps using pf2 syntax REQUIRED
Illustration of an editor hover tooltip shown over a proof annotation. The header names the theorem and shows a green Verified status, circled by callout one. Below, it lists the precondition and the postcondition and renders the proof body as mathematics.
Illustration: hovering a proof annotation renders the full statement and proof, with the verification status in the header. Backbuild Prove is launching soon.

Comment Syntax by Language

The prove:pf2 marker goes inside your language's documentation comment syntax. Annotations always live in documentation comments, not regular comments.

Language Comment Syntax
Rust /// prove:pf2("name") or /* prove:pf2("name") ... */
TypeScript, JavaScript /** prove:pf2("name") ... */ (JSDoc)
Python # prove:pf2("name") (line prefix)
Objective-C /* prove:pf2("name") ... */ or // prove:pf2("name")
Dart /// prove:pf2("name") or /* prove:pf2("name") ... */
Go /* prove:pf2("name") ... */
Java /** prove:pf2("name") ... */ (Javadoc)
C# /* prove:pf2("name") ... */
C++ /* prove:pf2("name") ... */
C /* prove:pf2("name") ... */
Swift /* prove:pf2("name") ... */
Kotlin /** prove:pf2("name") ... */ (KDoc)
Haskell {- prove:pf2("name") ... -} or -- prove:pf2("name")
Ada -- prove:pf2("name") (line prefix)
Lua -- prove:pf2("name") (line prefix)
SQL -- prove:pf2("name") (line prefix)
x86_64 Assembly (GAS, .s/.S) # prove:pf2("name") (line prefix)
x86_64 Assembly (NASM, .asm) ; prove:pf2("name") (line prefix)
ARM64 Assembly // prove:pf2("name") or /* prove:pf2("name") ... */
WebAssembly ;; prove:pf2("name") (line prefix)

Language Examples

Rust

/// prove:pf2("checked_add_safe")
/// \[
/// \begin{sig} fn checked_add(a: u64, b: u64) -> Option<u64> \end{sig}
/// \begin{pre} a : \mathbb{N} \land b : \mathbb{N} \end{pre}
/// \begin{post} \text{result} = \text{Some}(a + b) \lor \text{result} = \text{None} \end{post}
/// \begin{proof}
/// \step{1}{\text{checked\_add\_safe}
///   = (\lambda\, a\, b.\; a + b)}
///   \pf\ By DEFINE.
/// \step{2}{ a + b = b + a }
///   \pf\ By ADD\_SYM.
/// \qedstep
/// \begin{proof} \pf\ By \stepref{1} and \stepref{2}. \end{proof}
/// \end{proof}
/// \]
fn checked_add(a: u64, b: u64) -> Option<u64> {
    a.checked_add(b)
}

TypeScript / JavaScript

/** prove:pf2("array_sum_non_negative")
\[
\begin{sig} function sumArray(xs: number[]): number \end{sig}
\begin{pre}
  \forall\, i.\; 0 \leq i < \text{len}(xs)
    \Rightarrow xs[i] \geq 0
\end{pre}
\begin{post} \text{result} \geq 0 \end{post}
\begin{proof}
\step{1}{ \text{sum}([]) = 0 }
  \pf\ By definition of sum over empty list.
\step{2}{ \forall\, x \geq 0,\; s \geq 0.\;
  x + s \geq 0 }
  \pf\ By LE\_ADD.
\step{3}{ \text{sum}(xs) \geq 0 }
  \pf\ By induction on \text{len}(xs),
  \stepref{1}, and \stepref{2}.
\qedstep
  \begin{proof} \pf\ By \stepref{3}. \end{proof}
\end{proof}
\]
*/
function sumArray(xs: number[]): number {
  return xs.reduce((acc, x) => acc + x, 0);
}

Python

Python has no block comments, so annotations use consecutive # line comments directly above the function. (Docstrings are string literals, not comments, and are not scanned for annotations.)

# prove:pf2("binary_search_correct")
# \[
# \begin{pre}
#   \text{sorted}(arr) \land \text{len}(arr) > 0
# \end{pre}
# \begin{post}
#   \text{result} \neq \text{None}
#     \Rightarrow arr[\text{result}] = target
# \end{post}
# \begin{proof}
# \step{1}{ \text{lo} \leq \text{hi}
#   \Rightarrow \text{mid} \in [\text{lo}, \text{hi}] }
#   \pf\ By definition of integer division.
# \step{2}{ arr[\text{mid}] = target
#   \Rightarrow \text{result} = \text{mid} }
#   \pf\ By direct comparison.
# \step{3}{ \text{hi} - \text{lo}
#   \text{ decreases on each iteration} }
#   \pf\ By case split on arr[\text{mid}] vs target.
# \qedstep
#   \begin{proof} \pf\ By \stepref{1}, \stepref{2}, and \stepref{3}. \end{proof}
# \end{proof}
# \]
def binary_search(arr: list[int], target: int) -> int | None:
    lo, hi = 0, len(arr) - 1
    while lo <= hi:
        mid = (lo + hi) // 2
        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            lo = mid + 1
        else:
            hi = mid - 1
    return None

Go

/* prove:pf2("clamp_in_range")
\[
\begin{pre}
  \text{lo} \leq \text{hi}
\end{pre}
\begin{post}
  \text{lo} \leq \text{result} \leq \text{hi}
\end{post}
\begin{proof}
\step{1}{ x < \text{lo}
  \Rightarrow \text{result} = \text{lo} }
  \pf\ By first branch.
\step{2}{ x > \text{hi}
  \Rightarrow \text{result} = \text{hi} }
  \pf\ By second branch.
\step{3}{ \text{lo} \leq x \leq \text{hi}
  \Rightarrow \text{result} = x }
  \pf\ By default branch.
\qedstep
  \begin{proof} \pf\ By \stepref{1}, \stepref{2}, and \stepref{3},
  \text{result} \in [\text{lo}, \text{hi}]. \end{proof}
\end{proof}
\]
*/
func Clamp(x, lo, hi int) int {
    if x < lo {
        return lo
    }
    if x > hi {
        return hi
    }
    return x
}

Java

/** prove:pf2("abs_non_negative")
\[
\begin{pre} x \neq \text{Integer.MIN\_VALUE} \end{pre}
\begin{post} \text{result} \geq 0 \end{post}
\begin{proof}
\step{1}{ x \geq 0 \Rightarrow \text{result} = x \geq 0 }
  \pf\ By first branch.
\step{2}{ x < 0 \Rightarrow \text{result} = -x > 0 }
  \pf\ By second branch and precondition.
\qedstep
  \begin{proof} \pf\ By \stepref{1} and \stepref{2}. \end{proof}
\end{proof}
\]
*/
public static int abs(int x) {
    return x >= 0 ? x : -x;
}

C#

/* prove:pf2("max_returns_larger")
\[
\begin{proof}
\step{1}{ a \geq b \Rightarrow \text{result} = a }
  \pf\ By ternary true branch.
\step{2}{ a < b \Rightarrow \text{result} = b }
  \pf\ By ternary false branch.
\qedstep
  \begin{proof} \pf\ By \stepref{1} and \stepref{2}. \end{proof}
\end{proof}
\]
*/
public static int Max(int a, int b) => a >= b ? a : b;

C++

/* prove:pf2("swap_values")
\[
\begin{post} a' = b \land b' = a \end{post}
\begin{proof}
\step{1}{ \text{temp} = a }
  \pf\ By assignment.
\step{2}{ a' = b }
  \pf\ By second assignment.
\step{3}{ b' = \text{temp} = a }
  \pf\ By \stepref{1} and third assignment.
\qedstep
  \begin{proof} \pf\ By \stepref{2} and \stepref{3}. \end{proof}
\end{proof}
\]
*/
void swap(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}

Swift

/* prove:pf2("is_even_correct")
\[
\begin{proof}
\step{1}{ n \mod 2 = 0 \Rightarrow \text{result} = \text{true} }
  \pf\ By definition of even.
\step{2}{ n \mod 2 \neq 0 \Rightarrow \text{result} = \text{false} }
  \pf\ By negation.
\qedstep
  \begin{proof} \pf\ By \stepref{1} and \stepref{2}. \end{proof}
\end{proof}
\]
*/
func isEven(_ n: Int) -> Bool {
    return n % 2 == 0
}

Kotlin

/** prove:pf2("safe_divide")
\[
\begin{pre} b \neq 0 \end{pre}
\begin{proof}
\step{1}{ b \neq 0 \Rightarrow a / b \text{ is defined} }
  \pf\ By precondition.
\qedstep
  \begin{proof} \pf\ By \stepref{1}. \end{proof}
\end{proof}
\]
*/
fun divide(a: Int, b: Int): Int = a / b

Haskell

{- prove:pf2("reverse_involutive")
\[
\begin{proof}
\step{1}{ \text{reverse}(\text{reverse}([])) = [] }
  \pf\ By base case.
\step{2}{ \text{reverse}(\text{reverse}(x:xs))
  = x : \text{reverse}(\text{reverse}(xs)) }
  \pf\ By inductive step and reverse definition.
\qedstep
  \begin{proof} \pf\ By induction, \stepref{1} and \stepref{2}. \end{proof}
\end{proof}
\]
-}
myReverse :: [a] -> [a]
myReverse [] = []
myReverse (x:xs) = myReverse xs ++ [x]

Lua

-- prove:pf2("table_count_positive")
-- \[
-- \begin{proof}
-- \step{1}{ \text{count}(\{}) = 0 }
--   \pf\ By empty table.
-- \qedstep
-- \begin{proof} \pf\ By \stepref{1}. \end{proof}
-- \end{proof}
-- \]
function count(t)
  local n = 0
  for _ in pairs(t) do n = n + 1 end
  return n
end

SQL

-- prove:pf2("tenant_isolation")
-- \[
-- \begin{pre} \text{tenant\_id} \neq \text{NULL} \end{pre}
-- \begin{post}
--   \forall\, r \in \text{result}.\;
--     r.\text{tenant\_id} = \text{p\_tenant\_id}
-- \end{post}
-- \begin{proof}
-- \step{1}{ \text{WHERE tenant\_id = p\_tenant\_id}
--   \text{ filters all rows} }
--   \pf\ By SQL WHERE semantics.
-- \qedstep
-- \begin{proof} \pf\ By \stepref{1}. \end{proof}
-- \end{proof}
-- \]
CREATE FUNCTION get_orders(p_tenant_id UUID)
RETURNS SETOF orders AS $$
  SELECT * FROM orders WHERE tenant_id = p_tenant_id;
$$ LANGUAGE sql;

Pre/Post Contracts

Preconditions and postconditions define a function’s formal contract. The kernel verifies that the proof is consistent with both the contract and the parser’s translation of the code.

/* prove:pf2("divide_safe")
\[
\begin{pre}
  \forall\, a : \text{Int}, b : \text{Int}.\; b \neq 0
\end{pre}
\begin{post}
  \text{divide}(a, b) \cdot b + (a \mod b) = a
\end{post}
\begin{proof}
\step{1}{ \text{divide}(a, b) \cdot b + (a \mod b) = a }
  \pf\ By definition of integer division (Euclidean property).
\qedstep
  \begin{proof} \pf\ By \stepref{1}. \end{proof}
\end{proof}
\]
*/

The \begin{pre} block declares what must hold before the function executes. The \begin{post} block declares what the function guarantees when it returns. The proof must show that the postcondition follows from the precondition and the code’s behavior.

Documenting External Dependencies

When a proof reasons about external code that Backbuild Prove cannot translate directly (for example a third-party library, native FFI, or system call), document the dependency in the human-readable \begin{doc} section and cite it in your proof steps. This keeps the assumption explicit and auditable while the kernel verifies the steps that build on it.

/* prove:pf2("encrypt_data")
\[
\begin{doc}
  Assumes OpenSSL AES-256-GCM provides IND-CPA security.
\end{doc}
\begin{proof}
\step{1}{ \text{AES-256-GCM} \text{ provides authenticated encryption} }
  \pf\ By the documented OpenSSL AES-256-GCM assumption.
\step{2}{ \text{result} \text{ is ciphertext} }
  \pf\ By \stepref{1} and function implementation.
\qedstep
  \begin{proof} \pf\ By \stepref{2}. \end{proof}
\end{proof}
\]
*/

Content Hash Caching

Backbuild Prove computes a content hash for each annotation. When you re-run verification, annotations whose content has not changed since their last successful verification are returned immediately with a Cached status. Only modified or new annotations are sent to the HOL kernel for re-verification.

This means that large projects with hundreds of annotations can re-verify in seconds when only a few files have changed. Any change to the annotation content, including whitespace changes inside the proof, invalidates the cache and triggers re-verification.

When the Parser Cannot Translate Code

Some code constructs cannot be translated into the kernel’s term language. Common cases include:

  • Inline assembly
  • FFI (Foreign Function Interface) calls
  • Macros and code generation
  • Reflective or metaprogramming code
  • Dynamic dispatch through runtime type information

For functions that contain untranslatable constructs, document the untranslatable portion as an explicit assumption in the \begin{doc} section and reason about it in your proof steps. This preserves soundness while making the dependency explicit and auditable.

AI Agents and pf2

AI coding agents do not need special training to write pf2 annotations. The Backbuild system teaches agents through three mechanisms:

  • Examples: the system provides annotated code samples that agents can follow.
  • Documentation: agents receive references to the pf2 syntax guide and the available theorem library.
  • Instructive error messages: when verification fails, the error message explains exactly what went wrong and what the kernel expected. The agent reads the error, understands the issue, and self-corrects.

This teach-by-feedback approach means any AI coding agent can learn to write correct pf2 annotations through normal tool use.

How do I write a proof on a normal function?
Put the contract and the reasoning in a prove:pf2 comment above the function: a precondition for what must hold on entry, a postcondition for what the function guarantees, and numbered steps that justify the postcondition, closed by a QED step. Start with the smallest property that matters and grow it. The Getting Started example is a complete one you can copy.

What if my function calls something the kernel cannot see, like a foreign function or inline assembly?
Record that boundary as an explicit assumption in the \begin{doc} section and cite it in your steps. The kernel still verifies everything built on the assumption, and a reader sees exactly what was taken on trust. Functions that contain untranslatable constructs otherwise report parser_skip; see Troubleshooting.

Will unchanged code re-verify every time?
No. Each annotation is content-hashed, and an unchanged annotation returns a Cached result instantly. Only new or modified annotations are sent to the kernel, so re-verifying a large project after a small edit is fast.

Next Steps