This guide covers how to write specifications, prove invariants, and establish refinement using Leslie. It assumes familiarity with TLA+ or TLA concepts and basic Lean 4 proficiency.
- Core Concepts
- Writing a Specification
- Random Simulation
- Proving Invariants
- Refinement
- Layered Refinement (CIVL)
- TLA Formula Syntax
- Proof Tactics Reference
- Theorem Reference
- Proof Patterns Cookbook
For a tutorial on modeling and verifying round-based distributed algorithms using the Heard-Of model, communication closure, and cutoff theorems, see:
Round-Based Algorithms Tutorial
This tutorial covers:
- The Heard-Of model and why communication closure simplifies verification
- How to specify round-based algorithms (
RoundAlg,RoundSpec) - Proving safety via lock invariants and pigeonhole arguments
- The cutoff theorem: reducing verification for all n to finite model checking
- A worked example with the OneThirdRule consensus algorithm
Related design documents:
- Model-Checking Tactic Plan — automating the
small-n verification step via
native_decide - Zero-One Rule — reducing unbounded value domains to binary for value-oblivious threshold protocols
Leslie uses a shallow embedding of TLA in Lean 4. The fundamental types are:
| Type | Definition | Meaning |
|---|---|---|
exec σ |
Nat → σ |
Infinite execution (sequence of states) |
pred σ |
exec σ → Prop |
TLA formula (predicate over executions) |
action σ |
σ → σ → Prop |
Two-state relation (transition) |
A state predicate f : σ → Prop is lifted to a TLA predicate via
state_pred f, which checks f on the first state of an execution.
An action predicate a : action σ is lifted via action_pred a, which
checks a on the first two states.
| Operator | Notation | Definition |
|---|---|---|
| Always | □ p |
∀ k, p (e.drop k) |
| Eventually | ◇ p |
∃ k, p (e.drop k) |
| Next | ◯ p |
p (e.drop 1) |
| Until | p 𝑈 q |
Standard until |
| Leads-to | p ↝ q |
□(p → ◇q) |
| Weak fairness | 𝒲ℱ a |
□(□ Enabled a → ◇ ⟨a⟩) |
| Notation | Meaning |
|---|---|
|‑tla‑ p |
p is valid (holds for all executions) |
p |‑tla‑ q |
p implies q (for all executions) |
e |=tla= p |
Execution e satisfies p |
The recommended way to write a specification is using ActionSpec, which
separates a protocol into named actions with explicit gates (preconditions).
import Leslie.Action
open TLA
namespace MyProtocol
-- State type: use a structure with all protocol state
structure MyState where
counter : Nat
flag : Bool
-- Action index: enumerate all actions
inductive MyAction where
| increment | resetdef mySpec : ActionSpec MyState MyAction where
init := fun s => s.counter = 0 ∧ s.flag = false
actions := fun
| .increment => {
gate := fun s => s.flag = false -- precondition
transition := fun s s' => -- effect
s' = { s with counter := s.counter + 1 }
}
| .reset => {
gate := fun s => s.counter > 0
transition := fun s s' =>
s' = { counter := 0, flag := true }
}Deterministic transitions: Use s' = { s with field := value } for
updates that modify specific fields. The { s with ... } notation preserves
all fields not explicitly listed.
Non-deterministic transitions: Use existentials:
transition := fun s s' => ∃ v, s' = { s with value := v }Constrained non-determinism (e.g., Paxos phase-2a):
transition := fun s s' => ∃ v, s' = { s with prop := some v } ∧
(∀ w, someCondition s w → v = w)Boolean message flags: For message-passing protocols, model messages as
Boolean flags (sent : Bool) rather than message sets. This avoids Finset
reasoning and is sufficient for fixed-size instances.
Fixed instances: Use concrete enumerations (e.g., ThreadId with t1 | t2)
rather than parameterizing over N. This makes proofs tractable via case
analysis.
ActionSpec converts to a Spec automatically:
-- These are available:
mySpec.toSpec -- Spec with next = ∃ i, (actions i).fires s s'
mySpec.toSpec.safety -- ⌜init⌝ ∧ □⟨next⟩
mySpec.toSpec.safety_stutter -- ⌜init⌝ ∧ □⟨next ∨ id⟩Before investing in formal proofs, you can test your protocol with random simulation. The simulator performs random walks through the state space, checking an invariant at every step, and reports a counterexample trace if a violation is found.
ActionSpec definitions use Prop-valued predicates (gates, transitions),
which are not directly executable. To simulate, you provide a Simulable
instance — an executable mirror of your ActionSpec with Bool-valued gates
and concrete transition functions.
import Leslie.Simulate
The Simulable typeclass has four fields, each mirroring a component of
ActionSpec:
Simulable field |
Mirrors | Type |
|---|---|---|
init |
ActionSpec.init |
IO σ — generate a concrete initial state |
actions |
enumeration of ι |
List ι — all action indices |
enabled |
GatedAction.gate |
ι → σ → Bool — executable gate check |
step |
GatedAction.transition |
ι → σ → IO σ — executable transition |
The step field uses IO so that nondeterministic transitions (e.g., choosing
a value to write) can make random choices via IO.rand.
-- Derive Repr for state/action types so traces can be printed
deriving instance Repr for MyState
deriving instance Repr for MyAction
instance : TLA.Sim.Simulable MyState MyAction where
init := pure { counter := 0, flag := false }
actions := [.increment, .reset]
enabled := fun act s => match act with
| .increment => s.flag == false
| .reset => s.counter > 0
step := fun act s => pure (match act with
| .increment => { s with counter := s.counter + 1 }
| .reset => { counter := 0, flag := true })For nondeterministic transitions, use IO.rand inside step:
step := fun act s => match act with
| .writeOk r => do
-- Choose a random value (mirrors ∃ v, s' = { s with ... })
let v ← if (← IO.rand 0 1) == 0 then pure Value.v0 else pure Value.v1
pure { s with journal := s.journal ++ [v], ... }
| ...Write your invariant as a Bool-valued function:
def myInv (s : MyState) : Bool :=
s.counter ≤ 10This should be the executable version of the Prop-valued invariant you
intend to verify later.
-- Check invariant across 1000 random traces of up to 100 steps each
#eval TLA.Sim.simulate (ι := MyAction) myInv
{ numTraces := 1000, maxSteps := 100 }On success:
OK: 1000 traces × ≤100 steps, 0 deadlocks, avg length 101
On violation, the full counterexample trace is printed:
VIOLATION in trace 42 at step 7!
Counterexample:
init
→ { counter := 0, flag := false }
MyAction.increment
→ { counter := 1, flag := false }
...
| Function | Signature | Purpose |
|---|---|---|
simulate |
[Simulable σ ι] [Repr σ] [Repr ι] → (σ → Bool) → Config → IO Bool |
Run traces, print results, return true if no violation |
randomWalk |
[Simulable σ ι] [Repr σ] [Repr ι] → Nat → IO Unit |
Print a single random trace (for protocol exploration) |
quickCheck |
[Simulable σ ι] → (σ → Bool) → Nat → Nat → IO Bool |
Silent check, returns Bool only |
runTrace |
[Simulable σ ι] → Config → (σ → Bool) → IO (TraceOutcome σ ι) |
Single trace returning structured result |
The Config structure has two fields:
| Field | Default | Meaning |
|---|---|---|
maxSteps |
10000 | Maximum steps per trace |
numTraces |
100 | Number of random traces |
Use randomWalk to generate a single trace and see how the protocol evolves:
#eval TLA.Sim.randomWalk (σ := MyState) (ι := MyAction) (steps := 20)This prints each action and resulting state, which is useful for understanding the protocol before writing invariants. If the protocol reaches a state with no enabled actions, it reports a deadlock.
- Write the
ActionSpec— define your state, actions, and spec. - Write a
Simulableinstance — mirror gates and transitions as executable code. Keep this right next to the spec so they stay in sync. - Explore with
randomWalk— sanity-check that the protocol behaves as expected. - Test invariants with
simulate— run thousands of traces to build confidence before attempting proofs. - Prove the invariant formally — use
init_invariantand the patterns in sections below.
If simulation finds a bug, fix the spec and re-run. This fast feedback loop avoids spending time on proofs for broken protocols.
- Deadlocks: If many traces deadlock, your protocol may have liveness
issues (states with no enabled actions). Inspect the trace with
randomWalkto understand why. - Coverage: Random simulation cannot replace verification — it may miss rare corner cases. But it catches most common bugs quickly.
- Nondeterminism: For protocols with existential transitions, the
stepfunction picks one random successor. Run many traces to cover different choices. - Performance: The simulator runs in
IOand is quite fast. 10,000 traces of 10,000 steps each (100M total steps) typically completes in seconds. - Separate files: Put
#evalstatements in a separate*Sim.leanfile (not imported from the rootLeslie.lean) so simulation doesn't run during everylake build. SeeLeslie/Examples/TwoPhaseCommitSim.leanfor an example.
An invariant is a state predicate that holds at every reachable state.
The primary tool for proving invariants:
theorem init_invariant :
(∀ s, init s → inv s) → -- inv holds initially
(∀ s s', next s s' → inv s → inv s') → -- inv is preserved
pred_implies spec.safety [tlafml| □ ⌜ inv ⌝] -- inv holds alwaysdef my_inv (s : MyState) : Prop :=
s.counter ≤ 10
theorem my_inv_holds :
pred_implies mySpec.toSpec.safety [tlafml| □ ⌜ my_inv ⌝] := by
apply init_invariant
· -- Base case: init → inv
intro s ⟨hcounter, hflag⟩
simp [my_inv, hcounter]
· -- Inductive step: next ∧ inv → inv'
intro s s' ⟨i, hfire⟩ hinv
cases i <;> simp [mySpec, GatedAction.fires] at hfire <;>
obtain ⟨hgate, htrans⟩ := hfire <;> subst htrans <;>
simp [my_inv] at * <;> omega- Destructure the action:
⟨i, hfire⟩gives you the action index and firing proof. - Case split on actions:
cases isplits into one goal per action. - Unfold and simplify:
simp [specName, GatedAction.fires] at hfireexposes the gate and transition. - Extract gate + transition:
obtain ⟨hgate, htrans⟩ := hfire - Substitute the transition:
subst htransreplacess'with the concrete struct update. - Close the goal: Usually
simp_all,omega, or manual case analysis.
For existential transitions (e.g., ∃ v, s.prop = some v ∧ s' = ...):
obtain ⟨hgate, v, hprop, htrans⟩ := hfire
subst htransRefinement proves that a concrete system implements an abstract specification: every behavior of the concrete system, when mapped through a state function, is a behavior of the abstract system.
-- Define the abstraction function
def abs_map (s : ConcreteState) : AbstractState := ...
-- Prove refinement
theorem my_refinement :
refines_via abs_map concrete.toSpec.safety abstract.toSpec.safety_stutter := by
apply refinement_mapping_stutter concrete.toSpec abstract.toSpec abs_map
· -- Init: concrete init → abstract init
intro s hinit ; ...
· -- Step: concrete step → abstract step or stutter
intro s s' ⟨i, hfire⟩
cases i <;> ...
· left ; exact ⟨abstractAction, gateProof, transitionProof⟩ -- real step
· right ; simp [abs_map] -- stutterWhen the abstraction function only works under an invariant of the concrete system:
theorem my_refinement :
refines_via abs_map concrete.toSpec.safety abstract.toSpec.safety_stutter := by
apply refinement_mapping_stutter_with_invariant
concrete.toSpec abstract.toSpec abs_map my_inv
· -- inv_init: init → inv
· -- inv_next: inv ∧ step → inv'
· -- init_preserved: init → abstract.init (abs_map s)
· -- step_sim: inv ∧ step → abstract step or stutter| Theorem | Use Case |
|---|---|
refinement_mapping_nostutter |
Every step maps to an abstract step (no stutter) |
refinement_mapping_stutter |
Steps may stutter (map to identity) |
refinement_mapping_with_invariant |
No stutter, but need an invariant |
refinement_mapping_stutter_with_invariant |
Stutter + invariant (most common) |
refinement_mapping_stutter_stutter |
Both specs allow stutter |
refines_via_trans |
Compose two refinements: (f : σ→τ, g : τ→υ) → (g∘f : σ→υ) |
For each concrete action, you show either:
Abstract step (left disjunct):
left
exact ⟨abstractActionIndex, gateProof, transitionProof⟩Stutter (right disjunct — abstract state doesn't change):
right
simp [abs_map] -- or: rfl, or manual proof that abs_map s = abs_map s'Leslie supports CIVL-style layered verification with mover types and Lipton reduction.
inductive MoverType where
| right -- commutes to the right of concurrent actions
| left -- commutes to the left
| both -- commutes both ways
| nonmover -- does not commuteRight-commutativity: a then b can be reordered to b then a:
def right_commutes (a b : GatedAction σ) : Prop :=
∀ s s₁ s₂, a.fires s s₁ → b.fires s₁ s₂ →
∃ s₁', b.fires s s₁' ∧ a.fires s₁' s₂Left-commutativity: b then a can be reordered to a then b:
def left_commutes (a b : GatedAction σ) : Prop :=
∀ s s₁ s₂, b.fires s s₁ → a.fires s₁ s₂ →
∃ s₁', a.fires s s₁' ∧ b.fires s₁' s₂The typical pattern: destructure both firings, construct the witness state.
theorem rc_action1_action2 :
right_commutes (spec.actions .action1) (spec.actions .action2) := by
intro s s₁ s₂ ⟨hga, htrans_a⟩ ⟨hgb, htrans_b⟩
subst htrans_a htrans_b
-- Construct the intermediate state (b fires first, then a)
exact ⟨{ s with field2 := newVal },
⟨hgb, rfl⟩, -- b fires from s
⟨hga, by ext <;> simp⟩⟩ -- a fires from intermediateVacuous commutativity (contradictory gates — the two actions can never fire in sequence):
theorem lc_leave1_enter2 :
left_commutes (spec.actions .leave1) (spec.actions .enter2) := by
intro s s₁ s₂ ⟨⟨_, _⟩, hs₁⟩ ⟨hga, _⟩
subst hs₁ ; simp [specName] at hga -- derives False from contradictory gatesActions only need to commute with actions of other threads:
def Layer.movers_valid_threaded [DecidableEq θ]
(l : Layer σ ι) (thread : ι → θ) : Prop :=
∀ i j, thread i ≠ thread j →
((l.mover i).isRight = true → right_commutes (l.spec.actions i) (l.spec.actions j)) ∧
((l.mover i).isLeft = true → left_commutes (l.spec.actions i) (l.spec.actions j))The library provides proved reduction theorems:
| Theorem | What it does |
|---|---|
right_movers_swap |
Sequence of right-movers swaps past one env step |
left_movers_absorb |
Sequence of left-movers absorbs preceding env step |
fragment_right_reduction |
Push env step before R*;rest |
fragment_left_reduction |
Push env step after prefix;L* |
lipton_reduction_right |
In R*;N;L*, push env after R* to before fragment |
lipton_reduction_left |
In R*;N;L*, push env before L* to after fragment |
Deterministic fetch-and-increment does not commute (swapping two acquires
gives different ticket values). The standard CIVL approach: introduce a layer
with non-deterministic allocation, prove it commutes, then show the
deterministic version refines it. See TicketLock.lean for this pattern.
Leslie provides a tlafml syntax category for readable formulas:
-- State predicates
[tlafml| ⌜ fun s => s.x > 0 ⌝] -- state_pred
[tlafml| ⌜ myPredicate ⌝]
-- Action predicates
[tlafml| ⟨ fun s s' => s'.x = s.x + 1 ⟩] -- action_pred
[tlafml| ⟨ myAction ⟩]
-- Pure (non-temporal) propositions
[tlafml| ⌞ True ⌟] -- pure_pred
-- Temporal operators
[tlafml| □ p] -- always
[tlafml| ◇ p] -- eventually
[tlafml| ◯ p] -- next/later
[tlafml| p 𝑈 q] -- until
[tlafml| p ↝ q] -- leads-to
[tlafml| p ⇒ q] -- always-implies
[tlafml| 𝒲ℱ a] -- weak fairness
-- Logical connectives
[tlafml| p ∧ q] [tlafml| p ∨ q]
[tlafml| ¬ p] [tlafml| p → q]
[tlafml| ∀ x, p x] [tlafml| ∃ x, p x]
-- Big operators
[tlafml| ⋀ x ∈ l, f x] -- big conjunction
[tlafml| ⋁ x ∈ l, f x] -- big disjunction| Tactic | Purpose |
|---|---|
tla_unfold |
Unfold core TLA definitions (always, eventually, state_pred, action_pred, etc.) |
tla_unfold' |
Like tla_unfold plus execution simplifications (exec.drop, exec.map) |
tla_unfold_simp |
Full unfold + simp |
tla_nontemporal_simp |
Simplify non-temporal parts, leaving temporal structure intact |
tla_intros |
Move hypothesis from right of |‑tla‑ to left (via impl_intro) |
tla_merge_always t1, t2, ... => h |
Merge multiple □ p₁, □ p₂, ... into □(p₁ ∧ p₂ ∧ ...) named h |
simp_finite_exec_goal |
Generalize e k and e (k+1) to fresh variables when the goal is finite-state |
| Tactic | When to use |
|---|---|
cases i |
Split on action index (one goal per action) |
simp [specName, GatedAction.fires] at hfire |
Unfold the spec definition to expose gate + transition |
obtain ⟨hgate, htrans⟩ := hfire |
Destructure firing into gate and transition |
subst htrans |
Substitute s' = { s with ... } into the goal |
simp_all |
Aggressive simplification using all hypotheses |
ext <;> simp |
Prove struct equality by extensionality |
omega |
Solve linear arithmetic over Nat/Int |
decide |
Solve decidable propositions (no free variables) |
by_cases h : condition |
Case split on a Boolean/decidable condition |
rcases h with ⟨a, b⟩ | ⟨c, d⟩ |
Recursive case split on disjunctions/existentials |
nofun |
Close impossible goals (e.g., none = some _) |
| Attribute | Collected lemmas for |
|---|---|
@[tlasimp_def] |
TLA definition unfolding |
@[execsimp] |
Execution operations (drop, map) |
@[tla_nontemporal_def] |
Non-temporal simplification |
@[tlasimp] |
General TLA simplification |
init_invariant
(hinit : ∀ s, init s → inv s)
(hstep : ∀ s s', next s s' → inv s → inv s')
: pred_implies (⌜init⌝ ∧ □⟨next⟩) (□⌜inv⌝)
| Theorem | Statement |
|---|---|
always_intro |
valid p ↔ valid (□ p) |
always_weaken |
□ p |‑tla‑ p |
always_monotone |
(p |‑tla‑ q) → (□ p |‑tla‑ □ q) |
always_and |
□(p ∧ q) ⟺ □p ∧ □q |
always_idem |
□□p ⟺ □p |
always_forall |
□(∀x. p x) ⟺ ∀x. □(p x) |
always_unroll |
□p ⟺ p ∧ ◯□p |
always_induction |
□p ⟺ p ∧ □(p → ◯p) |
| Theorem | Statement |
|---|---|
eventually_idem |
◇◇p ⟺ ◇p |
eventually_monotone |
(p |‑tla‑ q) → (◇p |‑tla‑ ◇q) |
not_always |
¬□p ⟺ ◇¬p |
not_eventually |
¬◇p ⟺ □¬p |
always_eventually_always |
□◇□p ⟺ ◇□p |
eventually_always_eventually |
◇□◇p ⟺ □◇p |
| Theorem | Statement |
|---|---|
leads_to_trans |
(p ↝ q) ∧ (q ↝ r) |‑tla‑ (p ↝ r) |
leads_to_conseq |
Weaken lhs, strengthen rhs |
leads_to_combine |
Combine two leads-to with invariant |
leads_to_strengthen_lhs |
□inv → (p ∧ inv ↝ q) |‑tla‑ (p ↝ q) |
wf1 : (p ∧ ⟨next⟩ ⇒ ◯(p∨q)) -- next preserves p or achieves q
∧ (p ∧ ⟨next⟩ ∧ ⟨a⟩ ⇒ ◯q) -- action a achieves q
∧ (p ⇒ Enabled a ∨ q) -- p implies a is enabled (or q)
∧ (□⟨next⟩ ∧ 𝒲ℱ a)
⊢ (p ↝ q)
| Theorem | Signature |
|---|---|
refinement_mapping_nostutter |
(init→init') → (step→step') → refines_via |
refinement_mapping_stutter |
(init→init') → (step→step'∨stutter) → refines_via |
refinement_mapping_with_invariant |
inv_init → inv_step → init' → step' → refines_via |
refinement_mapping_stutter_with_invariant |
Above + stuttering (most common) |
refines_via_trans |
refines_via f p q → refines_via g q r → refines_via (g∘f) p r |
safety_implies_safety_stutter |
spec.safety |‑tla‑ spec.safety_stutter |
| Theorem | Signature |
|---|---|
LayerRefinement.to_refines_via |
Layer refinement → spec refinement |
LayerRefinementInv.to_refines_via |
With invariant |
LayerRefinement.compose |
Chain two layer refinements |
LayerRefinement.compose3 |
Chain three layer refinements |
| Theorem | Signature |
|---|---|
right_movers_swap |
seq_run rs s s₁ → env s₁ s₂ → ∃ s₁', env s s₁' ∧ seq_run rs s₁' s₂ |
left_movers_absorb |
env s s₁ → seq_run ls s₁ s₂ → ∃ s₁', seq_run ls s s₁' ∧ env s₁' s₂ |
theorem my_invariant :
pred_implies mySpec.toSpec.safety [tlafml| □ ⌜ inv ⌝] := by
apply init_invariant
· intro s ⟨h1, h2, ...⟩ -- destructure init
simp [inv, *] -- or: constructor <;> intro <;> simp_all
· intro s s' ⟨i, hfire⟩ hinv -- i : action index, hfire : fires
cases i <;> -- one goal per action
simp [mySpec, GatedAction.fires] at hfire <;>
obtain ⟨hgate, htrans⟩ := hfire <;>
subst htrans <;>
simp [inv] at * <;>
simp_alltheorem refines :
refines_via abs concrete.toSpec.safety abstract.toSpec.safety_stutter := by
apply refinement_mapping_stutter_with_invariant
concrete.toSpec abstract.toSpec abs inv
· intro s hinit ; ... -- inv_init
· intro s s' hinv ⟨i, hfire⟩ ; ... -- inv_next
· intro s hinit ; ... -- init_preserved
· intro s s' hinv ⟨i, hfire⟩ -- step_simulation
cases i <;> simp [concrete, GatedAction.fires] at hfire <;>
obtain ⟨hgate, htrans⟩ := hfire <;> subst htrans
· right ; simp [abs] -- stutter case
· left ; exact ⟨.someAction, gate, trans⟩ -- real stepWhen two actions modify independent struct fields:
theorem rc_a_b : right_commutes (spec.actions .a) (spec.actions .b) := by
intro s s₁ s₂ ⟨hga, htrans_a⟩ ⟨hgb, htrans_b⟩
subst htrans_a htrans_b
exact ⟨{ s with fieldB := newB },
⟨hgb, rfl⟩,
⟨hga, by ext <;> simp⟩⟩theorem rc_acquire_other :
right_commutes (spec.actions .acquire1) (spec.actions .acquire2) := by
intro s s₁ s₂ ⟨hga, t₁, hs₁⟩ ⟨hgb, t₂, hs₂⟩
subst hs₁ hs₂
exact ⟨{ s with ticket2 := some t₂ },
⟨hgb, t₂, rfl⟩,
⟨hga, t₁, by ext <;> simp⟩⟩When the two actions can never fire consecutively (contradictory gates):
theorem lc_leave_enter :
left_commutes (spec.actions .leave1) (spec.actions .enter2) := by
intro s s₁ s₂ ⟨⟨_, _⟩, hs₁⟩ ⟨hga, _⟩
subst hs₁
simp [specName] at hga -- gate becomes contradictory after substtheorem movers_valid : layer.movers_valid_threaded threadAssign := by
intro i j hij ; constructor
· intro hr -- right-mover case
cases i <;> cases j <;>
simp [movers, MoverType.isRight, layer] at hr <;>
simp [threadAssign] at hij -- filter same-thread pairs
· exact rc_lemma1
· exact rc_lemma2
...
· intro hl -- left-mover case
cases i <;> cases j <;>
simp [movers, MoverType.isLeft, layer] at hl <;>
simp [threadAssign] at hij
· exact lc_lemma1
...When a phase-2b action might create a quorum:
by_cases hmaj : majority3 s.did2b_1_1 s.did2b_1_2 s.did2b_1_3 = true
· -- majority already existed → stutter (abstract state unchanged)
right ; simp [paxos_ref, hmaj, ...]
· -- no majority yet
by_cases hmaj' : majority3 true s.did2b_1_2 s.did2b_1_3 = true
· -- new majority just formed → abstract choose action
left ; exact ⟨.choose, gateProof, transProof⟩
· -- still no majority → stutter
right ; simp [paxos_ref, majority3, ...] at *- Add
@[ext]to structures you need extensionality on (forext <;> simp). - Use
set_option maxHeartbeats Nif proofs time out (default 200000). simp [specName]unfolds the spec; add it when gate predicates aren't reducing.- After
subst, struct field access is already reduced — don't add redundantsimp. List.mem_cons_selftakes no explicit arguments in Lean 4 v4.27.- For
decideto work, the goal must have no free variables. - Use
nofunto close goals likesome x = none → ...orFalse → ....
-
Create
Leslie/Examples/MyExample.leanwithimport Leslie.Action(orimport Leslie.Layersif using layers). -
Define your types, specs, and invariants inside a
namespace. -
Add
import «Leslie».Examples.MyExampletoLeslie.lean. -
Run
lake buildto verify everything type-checks. -
Follow the recommended workflow:
- Define the
ActionSpecand aSimulableinstance. - Create a separate
Leslie/Examples/MyExampleSim.leanwith#evalstatements to run the simulator (do not import this fromLeslie.lean). - Explore with
randomWalk, then test invariants withsimulate. - Once simulation passes, prove the invariant formally.
- Use
sorryfor hard proof obligations and fill them in incrementally.
- Define the