In this section

This guide shows how to say, in Lean, what an exported definition is proved to do, so that every generated package carries it: a claim is a theorem registered with @[lungo_claim], naming the exports it is about (its subjects) and the specifications it relates them to. The examples are the sort and session crates in lungo-rs’s examples directory.

Require lungo’s Lean library

The attributes are in lungo’s Lean library, the Lake package lungo in the lean directory of lungo-rs, released with lungo at lungo’s version. Require it from the project’s lakefile:

# lakefile.toml
[[require]]
name = "lungo"
git = "https://github.com/machinefabric/lungo-rs"
rev = "v<lungo version>"
subDir = "lean"

(or path = "…/lungo-rs/lean" for a checkout), run lake update once, commit the manifest, and import Lungo where claims are stated. A library of another schema version than the lungo you build with is refused (LNG0709). The attributes add records only: the compiled program is the same with or without them.

Mark the specifications

A specification is the declaration a claim relates its subjects to: a contract, a model, a state machine, a protocol. Give it @[lungo_spec "<kind>"]:

import Lungo

namespace Sorting

open Lungo.Spec

/-- Numbers in ascending order: the output is ordered and a permutation of the input. -/
@[lungo_spec "lungo.contract"]
noncomputable def SortsAscending : Contract (List Nat) (List Nat) where
  pre _ := True
  post xs ys := ys.Pairwise (fun a b => decide (a ≤ b)) ∧ ys.Perm xs

lungo’s kinds are lungo.relation, lungo.contract, lungo.model, lungo.state, lungo.protocol and lungo.property; any other namespace is a package’s own (acme.cost-model), carried without interpretation. The library’s helpers for writing specifications are Lungo.Spec.Contract, Lungo.Spec.BehavioralModel, Lungo.Spec.StateSpec, Lungo.Trace.Protocol and Lungo.Monitor (see lungo’s Lean library).

Make a specification that is only logical noncomputable: a contract or a protocol has nothing to run, and a computable definition of the root modules is exported like any other. An executable model (a reference implementation) can stay computable and be exported.

State the claim

Prove the theorem, then register it with the relation, the subjects after subject and the specifications after spec:

def sort (xs : List Nat) : List Nat := xs.mergeSort fun a b => decide (a ≤ b)

@[lungo_claim "lungo.satisfies" subject sort spec SortsAscending]
theorem sort_satisfies : Satisfies sort SortsAscending := by
  intro xs _
  refine ⟨List.pairwise_mergeSort ?_ ?_ xs, List.mergeSort_perm xs _⟩
  · intro a b c hab hbc; simp only [decide_eq_true_eq] at *; omega
  · intro a b; simp only [Bool.or_eq_true, decide_eq_true_eq]; omega

The attribute checks that the declaration is a theorem, that every subject is a definition (not a theorem) its statement mentions, that every specification has @[lungo_spec] and is mentioned, and, for lungo’s own relations, that the statement has the relation’s shape (after its leading ∀s):

Relation Statement
lungo.decides f … = true ↔ P …, the left side mentioning every subject
lungo.equals lhs = rhs, lhs mentioning every subject
lungo.roundtrip lhs = rhs, lhs mentioning every subject and rhs none, such as decode (encode x) = some x
lungo.satisfies Lungo.Spec.Satisfies f c or Lungo.Spec.SatisfiesExcept f c
lungo.refines Lungo.Spec.Refines impl spec
lungo.preserves Lungo.Spec.StateSpec.Preserves s exec or ….Implements s exec
lungo.monitors Lungo.Monitor.Sound m p
lungo.law anything mentioning every subject

A relation in another namespace (acme.cost-bound) is a package’s own: its shape is not checked. Anything the attribute refuses is an error in Lean; lungo checks the records it reads again, so a record written without the attribute is held to the same rules (LNG0701–LNG0705).

The session example claims that its step function implements a declarative state machine:

@[lungo_spec "lungo.state"]
noncomputable def Spec : StateSpec Sess Op Bool where
  init s := s = { isOpen := false, count := 0 }
  invariant _ := True
  step s op accepted s' := …

@[lungo_claim "lungo.preserves" subject step spec Spec]
theorem step_implements : Spec.Implements step := by …

Make assumptions explicit

When a claim holds only if the host behaves (its clock ticks, its store keeps what it is given), state that as an assumption (@[lungo_assumption], see how to declare facilities and assumptions) and take it as a hypothesis of the theorem:

@[lungo_claim "lungo.roundtrip" subject toTicks toSeconds]
theorem toSeconds_toTicks (h : Ticks) (seconds : Nat) : toSeconds (toTicks seconds) = seconds :=
  Nat.mul_div_cancel seconds h

The claim is still proved; it is reported as conditional on Timing.Ticks, in every package. An assumption is a proposition, never an axiom, so a proof can only use it as a hypothesis, where the report sees it.

Require claims in the build

Make the build fail when an export lacks a proved claim:

// build.rs
fn main() -> lungo_build::Result<()> {
    lungo_build::configure().require_claims("Sorting.sort").require_claims("Sorting.rank").compile_lean("lean")
}
# lungo.toml
[assurance]
require-claims = ["Sorting.sort", "Sorting.rank"]

A path selects the export it names and every export in it as a namespace; "." selects every export. An export it selects that is the subject of no proved claim fails the build with LNG0602; a path selecting nothing is a configuration error.

The trust policy applies to the evidence too: a claim whose theorem depends on sorry fails the build under deny_sorry (on by default) with LNG0601, and deny_axioms refuses evidence resting on non-standard axioms. With deny_sorry(false) such a claim is reported incomplete, and does not count as proved for require-claims. forbid-assumptions refuses claims resting on an assumption (LNG0603).

Keep laws out of the program

Theorems about exports can live in a module the program does not import, such as a Laws module that imports the program. Name it as an assurance module: lungo loads it for its records, and never compiles it into the program.

# lungo.toml
[lean]
assurance-modules = ["Formal.Laws"]

(Builder::assurance_module("Formal.Laws"), or --assurance-module Formal.Laws). A root module cannot also be an assurance module: its records are read anyway. A program that would need a value only an assurance module’s initialization computes is refused (LNG0710).

Read the claims back

lungo assurance reports each export’s claims, what its proofs do not cover, and what its claims assume:

$ lungo assurance Sorting.rank
program sorting: Lean 4.34.1 (…), lungo 1.88.0, assurance schema 1

Sorting.rank
  == Claims ==
    [proved] lungo.satisfies Sorting.rank_satisfies (of Sorting.RanksByScore)
    [proved] lungo.law Sorting.rank_stable
  == Trust ==
    (none)
  == Assumptions ==
    (none)
  == Facilities ==
    (none)

--format json writes it for tools; --claim-kind, --facility, --assumption and --trust-issue select exports; --policy checks the policies and exits with status 2 on a violation (see the lungo command). lungo check counts the claims, and lungo inspect <declaration> shows them for one declaration.

Every generated package carries the same records as assurance.json (see the assurance document), and reads them natively:

Language The document One export’s claims
Rust formal::__meta::assurance() formal::__meta::declaration("Formal.step").unwrap().assurance.claims
Go pkg.Assurance() pkg.Assurance().Export("Sorting.rank").Claims
Python pkg.ASSURANCE pkg.ASSURANCE.export("Sorting.rank").claims
Swift assurance (and assuranceJSON) assurance.export("Sorting.rank")?.claims
TypeScript ASSURANCE ASSURANCE.exports.find((e) => e.name === "Sorting.rank").claims
C <id>_assurance_json() (the JSON text)
let claim = sort::__meta::assurance().claim("Sorting.sort_satisfies").unwrap();
assert_eq!(claim.status, lungo::ClaimStatus::Proved);
assert_eq!(claim.specifications, ["Sorting.SortsAscending"]);