|
| 1 | +/- |
| 2 | +Copyright (c) 2026 Gaëtan Serré. All rights reserved. |
| 3 | +Released under Apache 2.0 license as described in the file LICENSE. |
| 4 | +Authors: Gaëtan Serré |
| 5 | +-/ |
| 6 | +module |
| 7 | + |
| 8 | +public import RandomDo.Tactic.IsMarkov.While.Termination |
| 9 | + |
| 10 | +/-! |
| 11 | +# A tactic for the termination of `while` loops |
| 12 | +
|
| 13 | +`terminates` proves the goal `Terminates f b` that `is_markov` hands back for a `while` loop, from |
| 14 | +an invariant, a variant and a probability given on the states of the loop. It applies a rule of |
| 15 | +`RandomDo.Tactic.IsMarkov.While.Termination`, unfolds the step of the loop on its successors, and |
| 16 | +leaves the remaining conditions as goals about the states only. |
| 17 | +
|
| 18 | +## Main declarations |
| 19 | +
|
| 20 | +* `loop_step`: simplifies a goal about one step of a loop into conditions on its successors. |
| 21 | +* `terminates`: proves the termination of a loop by one of the rules. |
| 22 | +-/ |
| 23 | + |
| 24 | +public meta section |
| 25 | + |
| 26 | +open Lean |
| 27 | + |
| 28 | +/-- `loop_step [h₁, …]` simplifies a goal about one step of a loop: a property that almost every |
| 29 | +successor of a state satisfies, the probability of a set of successors, or an arithmetic condition. |
| 30 | +It evaluates the step on its successors, with the additional simp lemmas `h₁, …` for the |
| 31 | +definitions of the program, splits on the conditions of the program, and tries to close the |
| 32 | +resulting goals, which only mention the state. -/ |
| 33 | +syntax (name := loopStep) "loop_step" (" [" term,* "]")? : tactic |
| 34 | + |
| 35 | +macro_rules |
| 36 | + | `(tactic| loop_step $[[$ls,*]]?) => do |
| 37 | + let ls : Array Term := (ls.map (·.getElems)).getD #[] |
| 38 | + `(tactic| ( |
| 39 | + try intro $(mkIdent `s) $(mkIdent `hs) |
| 40 | + try simp only at * |
| 41 | + try simp only [MeasureTheory.ae_iff] |
| 42 | + try split_ifs |
| 43 | + all_goals try norm_num [MeasureTheory.Measure.dirac_apply, Set.indicator_apply, |
| 44 | + ENNReal.toReal_add, ENNReal.toReal_ofReal', ENNReal.mul_eq_top, $[$ls:term],*] at * |
| 45 | + -- A measure followed by a deterministic successor is its image, and the probability of a set |
| 46 | + -- under the image is at least the probability of its preimage. |
| 47 | + all_goals try rw [MeasureTheory.Measure.bind_dirac_eq_map] |
| 48 | + all_goals try refine le_trans ?_ (ENNReal.toReal_mono (MeasureTheory.measure_ne_top _ _) |
| 49 | + (MeasureTheory.Measure.le_map_apply ?_ _)) |
| 50 | + all_goals try fun_prop |
| 51 | + all_goals try simp only [Set.preimage_ofPred_eq] at * |
| 52 | + all_goals try split_ifs |
| 53 | + all_goals try norm_num [ENNReal.toReal_add, ENNReal.toReal_ofReal', ENNReal.mul_eq_top] at * |
| 54 | + all_goals repeat' apply And.intro |
| 55 | + all_goals try first | done | trivial | assumption | omega | linarith | positivity)) |
| 56 | + |
| 57 | +/-- The invariant of `terminates`: the given property of the running states, whose stability is left |
| 58 | +as the goal `step`, or the property that always holds. -/ |
| 59 | +def invariant (P? : Option Term) : MacroM Term := |
| 60 | + match P? with |
| 61 | + | some P => `(({ running := $P, step := ?step } : MeasurableSpaceMonadWhile.LoopInvariant _)) |
| 62 | + | none => `(({ running := fun _ ↦ True |
| 63 | + step := fun _ _ ↦ Filter.Eventually.of_forall fun t ↦ by cases t <;> trivial } : |
| 64 | + MeasurableSpaceMonadWhile.LoopInvariant _)) |
| 65 | + |
| 66 | +/-- `terminates` proves the termination of a `while` loop, the goal `Terminates f b` that |
| 67 | +`is_markov` hands back (after `intro` of the parameters the loop depends on). |
| 68 | +
|
| 69 | +* `terminates (prob := ε)` applies `Terminates.mcIverMorgan_immediateEscape`: every step stops with |
| 70 | + probability at least `ε > 0`. |
| 71 | +* `terminates (variant := U) (bound := N) (prob := ε)` applies |
| 72 | + `Terminates.majumdarSathiyanarayana_variantRule`, with a variant `U : σ → ℕ` on the states, at |
| 73 | + most `N`, decreased with probability at least `ε > 0` by every step that does not stop. Stopping |
| 74 | + counts as decreasing `U`. |
| 75 | +* `(invariant := P)`, before the other arguments, restricts both to the states satisfying |
| 76 | + `P : σ → Prop`, which almost every step keeps. |
| 77 | +* `[h₁, …]`, after the other arguments, are simp lemmas for the definitions of the program. |
| 78 | +
|
| 79 | +The variant rule takes a variant `U'` on the states `ForInStep σ` of the transition system, with |
| 80 | +`Lo ≤ U' < Hi`, and a probability `> ε` of decreasing it. `terminates` applies it to |
| 81 | +`U' (done s) = 0` and `U' (yield s) = U s + 1`, between `Lo = 0` and `Hi = N + 2`, with `ε / 2`: |
| 82 | +* A step from `yield s` that stops goes to some `done s'`, and counts as decreasing `U'` only if |
| 83 | + `U' (done s') < U' (yield s)`. As `U` can be `0` on a running state (when the loop is about to |
| 84 | + stop, as `countdown` at `0`), the terminal states need a value below all the values of `U`: |
| 85 | + hence the shift of `U` by `1` on the running states, the terminal states taking `0`. A step to |
| 86 | + `yield s'` still decreases `U'` exactly when it decreases `U`. |
| 87 | +* On the invariant, `U s ≤ N`, so `0 ≤ U' ≤ N + 1`, and the strict upper bound of the rule is |
| 88 | + `N + 2`: one for the shift, one for passing from `≤` to `<`. |
| 89 | +* `(prob := ε)` asks for a probability at least `ε`, while the rule asks for one greater than its |
| 90 | + constant: a probability `≥ ε` is `> ε / 2`. |
| 91 | +
|
| 92 | +The conditions it cannot prove are left as goals about the states only. -/ |
| 93 | +syntax (name := terminatesTac) "terminates" (atomic(" (" &"invariant") " := " term ")")? |
| 94 | + (atomic(" (" &"variant") " := " term ")")? (atomic(" (" &"bound") " := " term ")")? |
| 95 | + " (" &"prob" " := " term ")" (" [" term,* "]")? : tactic |
| 96 | + |
| 97 | +macro_rules |
| 98 | + | `(tactic| terminates $[(invariant := $P?)]? (prob := $ε) $[[$ls,*]]?) => do |
| 99 | + let ls : Array Term := (ls.map (·.getElems)).getD #[] |
| 100 | + let I ← invariant P? |
| 101 | + `(tactic| ( |
| 102 | + intros |
| 103 | + refine MeasurableSpaceMonadWhile.Terminates.mcIverMorgan_immediateEscape $I $ε ?pos |
| 104 | + ?init ?stop |
| 105 | + all_goals try loop_step [$[$ls:term],*])) |
| 106 | + | `(tactic| terminates $[(invariant := $P?)]? (variant := $U) (bound := $N) (prob := $ε) |
| 107 | + $[[$ls,*]]?) => do |
| 108 | + let ls : Array Term := (ls.map (·.getElems)).getD #[] |
| 109 | + let I ← invariant P? |
| 110 | + let P ← P?.getDM `(fun _ ↦ True) |
| 111 | + `(tactic| ( |
| 112 | + intros |
| 113 | + -- `U` shifted by `1` on the running states, below which the terminal states are `0`: see the |
| 114 | + -- docstring for the bounds `0` and `N + 2` and for `ε / 2`. |
| 115 | + refine MeasurableSpaceMonadWhile.Terminates.majumdarSathiyanarayana_variantRule $I |
| 116 | + (fun t ↦ ForInStep.casesOn (motive := fun _ ↦ ℤ) t (fun _ ↦ 0) fun s ↦ (($U s : ℕ) : ℤ) + 1) |
| 117 | + 0 ((($N : ℕ) : ℤ) + 2) ($ε / 2) (half_pos ?pos) ?init ?bounds |
| 118 | + (fun $(mkIdent `s) $(mkIdent `hs) ↦ (half_lt_self ?pos).trans_le ?progress) ?measurable |
| 119 | + -- The bound of the lifted variant, from the bound `U ≤ N` on the states of the invariant. |
| 120 | + case' bounds => |
| 121 | + intro t $(mkIdent `hs) |
| 122 | + cases t with |
| 123 | + | done _ => dsimp only; omega |
| 124 | + | yield $(mkIdent `s) => |
| 125 | + replace $(mkIdent `hs) : ($P) $(mkIdent `s) := $(mkIdent `hs) |
| 126 | + try dsimp only at $(mkIdent `hs):ident ⊢ |
| 127 | + refine (fun h : ($U) $(mkIdent `s) ≤ ($N) ↦ by (try simp only [] at h); omega) ?_ |
| 128 | + try loop_step [$[$ls:term],*] |
| 129 | + -- The measurability of the lifted variant, from the measurability of `U`. |
| 130 | + case' measurable => first |
| 131 | + | exact Measurable.of_discrete |
| 132 | + | (refine MeasurableSpaceMonadWhile.measurable_casesOn measurable_const |
| 133 | + (((Measurable.of_discrete (f := fun n : ℕ ↦ (n : ℤ))).comp ?_).add_const 1) |
| 134 | + first | fun_prop | measurability | skip) |
| 135 | + try case' step => try loop_step [$[$ls:term],*] |
| 136 | + try case' init => try loop_step [$[$ls:term],*] |
| 137 | + try case' pos => try loop_step [$[$ls:term],*] |
| 138 | + try case' progress => try loop_step [$[$ls:term],*])) |
| 139 | + | `(tactic| terminates $[(invariant := $_)]? (variant := $_) (prob := $_) $[[$_,*]]?) => |
| 140 | + Macro.throwError "terminates: a variant needs a bound, given by `(bound := N)`" |
| 141 | + | `(tactic| terminates $[(invariant := $_)]? (bound := $_) (prob := $_) $[[$_,*]]?) => |
| 142 | + Macro.throwError "terminates: a bound needs a variant, given by `(variant := U)`" |
| 143 | + |
| 144 | +end |
0 commit comments