-- File: lean/ShortestPathSpec.lean -- Shortest Path (Dijkstra/Bellman-Ford) Specification -- Agents migrate a C++ Dijkstra to Rust. -- Graph: List (from, to, weight) with non-negative Nat weights. namespace ShortestPathSpec abbrev Graph := List (Nat × Nat × Nat) -- (from, to, weight) -- Safe index into a list (returns none if out of bounds) def listGet (l : List α) (i : Nat) : Option α := (l.drop i).head? -- Update distance at index i in the distance list def updateDist (dists : List (Option Nat)) (i d : Nat) : List (Option Nat) := List.zipWith (fun j curr => if j == i then match curr with | none => some d | some existing => some (min existing d) else curr) (List.range dists.length) dists -- Relax one edge (fr → to with weight w) def relaxEdge (dists : List (Option Nat)) (fr to w : Nat) : List (Option Nat) := match listGet dists fr with | some (some df) => updateDist dists to (df + w) | _ => dists -- One Bellman-Ford pass over all edges def relaxPass (dists : List (Option Nat)) (edges : Graph) : List (Option Nat) := edges.foldl (fun d e => relaxEdge d e.1 e.2.1 e.2.2) dists -- Dijkstra via n-1 Bellman-Ford iterations (correct for non-negative weights) def dijkstra (graph : Graph) (n src : Nat) : List (Option Nat) := let init := (List.range n).map (fun i => if i == src then some 0 else none) (List.range (n - 1)).foldl (fun d _ => relaxPass d graph) init -- Shortest distance between two nodes def shortestDist (graph : Graph) (n src dst : Nat) : Option Nat := match listGet (dijkstra graph n src) dst with | some (some d) => some d | _ => none -- listGet via getElem? theorem listGet_eq (l : List α) (i : Nat) : listGet l i = l[i]? := List.head?_drop -- (List.range n)[src]? = some src when src < n theorem range_getElem? (src n : Nat) (h : src < n) : (List.range n)[src]? = some src := by induction n generalizing src with | zero => omega | succ n ih => rw [List.range_succ] by_cases hlt : src < n · rw [List.getElem?_append_left (by simp [List.length_range, hlt])] exact ih src hlt · have heq : src = n := Nat.le_antisymm (Nat.lt_succ_iff.mp h) (by omega) subst heq rw [List.getElem?_append_right (by simp [List.length_range])] simp [Nat.sub_self] -- Initial dist list has some (some 0) at src theorem listGet_init (n src : Nat) (h : src < n) : listGet ((List.range n).map (fun i => if i == src then some 0 else none)) src = some (some 0) := by rw [listGet_eq, List.getElem?_map, range_getElem? src n h] simp -- updateDist preserves some (some 0) at position src theorem updateDist_preserves_zero (dists : List (Option Nat)) (i d src : Nat) (h : listGet dists src = some (some 0)) : listGet (updateDist dists i d) src = some (some 0) := by rw [listGet_eq] at h ⊢ have hlen : src < dists.length := by rcases Nat.lt_or_ge src dists.length with lt | ge · exact lt · exact absurd h (by simp [List.getElem?_eq_none ge]) unfold updateDist rw [List.getElem?_zipWith, range_getElem? src dists.length hlen, h] -- Goal: some ((fun j curr => if j==i then … else curr) src (some 0)) = some (some 0) simp only -- beta-reduce by_cases h_eq : i = src · subst h_eq; simp · simp [beq_eq_false_iff_ne.mpr (Ne.symm h_eq)] -- relaxEdge preserves some (some 0) at src theorem relaxEdge_preserves_zero (dists : List (Option Nat)) (fr to w src : Nat) (h : listGet dists src = some (some 0)) : listGet (relaxEdge dists fr to w) src = some (some 0) := by unfold relaxEdge split · exact updateDist_preserves_zero dists to _ src h · exact h -- relaxPass preserves some (some 0) at src theorem relaxPass_preserves_zero (dists : List (Option Nat)) (edges : Graph) (src : Nat) (h : listGet dists src = some (some 0)) : listGet (relaxPass dists edges) src = some (some 0) := by unfold relaxPass induction edges generalizing dists with | nil => simpa | cons e rest ih => simp only [List.foldl_cons] exact ih _ (relaxEdge_preserves_zero dists e.1 e.2.1 e.2.2 src h) -- foldl of relaxPass preserves the invariant (generalizing over init) theorem foldl_relaxPass_preserves_zero (rounds : Nat) (graph : Graph) (src : Nat) (init : List (Option Nat)) (h : listGet init src = some (some 0)) : listGet ((List.range rounds).foldl (fun d _ => relaxPass d graph) init) src = some (some 0) := by induction rounds generalizing init with | zero => simpa | succ k ih => rw [List.range_succ, List.foldl_append] simp only [List.foldl_cons, List.foldl_nil] exact relaxPass_preserves_zero _ graph src (ih _ h) -- Self-distance is always 0 for valid source nodes theorem self_distance_zero (graph : Graph) (n src : Nat) (h : src < n) : shortestDist graph n src src = some 0 := by let init := (List.range n).map (fun i => if i == src then some 0 else none) unfold shortestDist rw [show dijkstra graph n src = (List.range (n - 1)).foldl (fun d _ => relaxPass d graph) init from rfl, foldl_relaxPass_preserves_zero (n - 1) graph src init (listGet_init n src h)] -- DRT checks example : shortestDist [(0, 1, 5), (1, 2, 3)] 3 0 2 = some 8 := by native_decide example : shortestDist [(0, 1, 5)] 3 0 2 = none := by native_decide example : shortestDist [] 3 0 0 = some 0 := by native_decide example : shortestDist [(0, 1, 1), (0, 2, 4), (1, 2, 2)] 3 0 2 = some 3 := by native_decide example : dijkstra [(0, 1, 5), (1, 2, 3)] 3 0 = [some 0, some 5, some 8] := by native_decide end ShortestPathSpec