Skip to content

Instantly share code, notes, and snippets.

@lovely-error
Created September 2, 2026 15:19
Show Gist options
  • Select an option

  • Save lovely-error/c04450532856a39a2afb900ac330c0a2 to your computer and use it in GitHub Desktop.

Select an option

Save lovely-error/c04450532856a39a2afb900ac330c0a2 to your computer and use it in GitHub Desktop.
import Std
import Lean.Elab.Tactic.Omega
/-!
# The dining philosophers problem, for five philosophers
This file separates three properties which are easy to conflate:
* `fork_has_at_most_one_owner` is **mutual exclusion**: one fork cannot be held
by two philosophers.
* `step_is_one_fork_pickup` says that picking up forks is sequential: a pickup
transition acquires exactly one fork, never two atomically.
* `HasForwardMove` is a local **progress** property: some legal pickup can occur.
The first two safety properties do not imply the third. In the usual
"take the left fork, then wait for the right fork" protocol, every philosopher
can take their left fork. At that point every right fork is owned by the next
philosopher, nobody is eating, and no pickup transition is enabled.
`every_left_pick_permutation_deadlocks` proves that this does not depend on the
order in which the five philosophers make their first pickup.
-/
namespace DiningPhilosophers
/-- The integers `0, 1, 2, 3, 4`, with the bound carried in the type. -/
abbrev Philosopher := Fin 5
/-- Forks are also numbered `0, 1, 2, 3, 4`. -/
abbrev Fork := Fin 5
/--
A fixed-size array of five forks. `s f = none` means that fork `f` is on the
table; `s f = some p` means that philosopher `p` holds it.
Using a function on `Fin 5` rather than Lean's mutable `Array` gives exactly the
same mathematical object while making extensional proofs straightforward.
`State.toArray` below supplies an executable array view.
-/
abbrev State := Fork → Option Philosopher
def State.toArray (s : State) : Array (Option Philosopher) :=
Array.ofFn s
/-- Philosopher `p`'s left fork has the same number as `p`. -/
def leftFork (p : Philosopher) : Fork := p
/-- Philosopher `p`'s right fork is the next fork around the circular table. -/
def rightFork (p : Philosopher) : Fork :=
⟨(p.val + 1) % 5, Nat.mod_lt _ (by decide)⟩
theorem rightFork_ne_self (p : Philosopher) : rightFork p ≠ p := by
revert p
native_decide
def initial : State := fun _ => none
def Holds (s : State) (p : Philosopher) (f : Fork) : Prop :=
s f = some p
def ForkFree (s : State) (f : Fork) : Prop :=
s f = none
/-- Replacing one fork's entry models one atomic pickup. -/
def assign (s : State) (f : Fork) (p : Philosopher) : State :=
fun g => if g = f then some p else s g
/-- Mutual exclusion follows from storing only one optional owner per fork. -/
theorem fork_has_at_most_one_owner
(s : State) (f : Fork) (p q : Philosopher)
(hp : Holds s p f) (hq : Holds s q f) : p = q := by
exact Option.some.inj (hp.symm.trans hq)
/-- Which hand/fork a philosopher is trying to acquire. -/
inductive Hand where
| left
| right
deriving DecidableEq, Repr
structure Pickup where
philosopher : Philosopher
hand : Hand
deriving DecidableEq, Repr
/--
The deliberately naive protocol studied here:
* a left fork may be taken when it is free;
* a right fork may be taken only after the philosopher holds their left fork.
There is no "put the left fork back while waiting" transition. This is the
standard protocol whose circular wait produces a deadlock.
-/
def CanTakeLeft (s : State) (p : Philosopher) : Prop :=
ForkFree s (leftFork p)
def CanTakeRight (s : State) (p : Philosopher) : Prop :=
Holds s p (leftFork p) ∧ ForkFree s (rightFork p)
instance (s : State) (p : Philosopher) : Decidable (CanTakeLeft s p) := by
unfold CanTakeLeft ForkFree
infer_instance
instance (s : State) (p : Philosopher) : Decidable (CanTakeRight s p) := by
unfold CanTakeRight Holds ForkFree
infer_instance
/-- One atomic transition; its successful branches update precisely one fork. -/
def step (s : State) (a : Pickup) : Option State :=
match a.hand with
| .left =>
if CanTakeLeft s a.philosopher then
some (assign s (leftFork a.philosopher) a.philosopher)
else
none
| .right =>
if CanTakeRight s a.philosopher then
some (assign s (rightFork a.philosopher) a.philosopher)
else
none
/-- A relation spelling out what it means for a transition to acquire one fork. -/
def OneForkPickup (s t : State) : Prop :=
∃ f p, ForkFree s f ∧ t = assign s f p
theorem step_is_one_fork_pickup {s t : State} {a : Pickup}
(h : step s a = some t) : OneForkPickup s t := by
rcases a with ⟨p, hand⟩
cases hand with
| left =>
simp only [step] at h
split at h
next enabled =>
injection h with ht
exact ⟨leftFork p, p, enabled, ht.symm⟩
next => simp at h
| right =>
simp only [step] at h
split at h
next enabled =>
injection h with ht
exact ⟨rightFork p, p, enabled.2, ht.symm⟩
next => simp at h
/-- A direct formalization of "nobody picks two forks at the same time". -/
def OneForkAtATime : Prop :=
∀ ⦃s t : State⦄ ⦃a : Pickup⦄, step s a = some t → OneForkPickup s t
theorem model_obeys_one_fork_at_a_time : OneForkAtATime := by
intro s t a h
exact step_is_one_fork_pickup h
/-- A philosopher eats exactly when they hold both adjacent forks. -/
def Eating (s : State) (p : Philosopher) : Prop :=
Holds s p (leftFork p) ∧ Holds s p (rightFork p)
/-- Local, scheduler-independent progress: at least one pickup is enabled. -/
def HasForwardMove (s : State) : Prop :=
∃ a t, step s a = some t
/-- A stuck state in which nobody has reached the eating state. -/
def Deadlocked (s : State) : Prop :=
(∀ p, ¬ Eating s p) ∧ ¬ HasForwardMove s
/-- The state after every philosopher has taken their left fork. -/
def allLeft : State := fun f => some f
theorem nobody_eats_in_allLeft (p : Philosopher) : ¬ Eating allLeft p := by
intro h
have same : rightFork p = p := Option.some.inj h.2
exact rightFork_ne_self p same
theorem allLeft_step_is_disabled (a : Pickup) : step allLeft a = none := by
rcases a with ⟨p, hand⟩
cases hand <;> simp [step, CanTakeLeft, CanTakeRight, ForkFree, allLeft]
theorem allLeft_has_no_forward_move : ¬ HasForwardMove allLeft := by
rintro ⟨a, t, h⟩
rw [allLeft_step_is_disabled] at h
contradiction
theorem allLeft_is_deadlocked : Deadlocked allLeft := by
exact ⟨nobody_eats_in_allLeft, allLeft_has_no_forward_move⟩
/--
The proposed one-fork-at-a-time rule is satisfied, yet a deadlocked state
exists. This is the logical statement that the rule is insufficient for
forward progress.
-/
theorem proposed_rule_does_not_guarantee_forward_progress :
OneForkAtATime ∧ ∃ s, Deadlocked s := by
exact ⟨model_obeys_one_fork_at_a_time, allLeft, allLeft_is_deadlocked⟩
/-!
## Every permutation of the first pickups has the same bad result
Since each philosopher's left fork has their own number, those five first
pickups never conflict. `afterLeftPickups order` is the exact ownership array
after the philosophers named in `order` have taken their left forks.
-/
def allPhilosophers : List Philosopher := [0, 1, 2, 3, 4]
def IsCompletePickupOrder (order : List Philosopher) : Prop :=
order.Perm allPhilosophers
def afterLeftPickups (order : List Philosopher) : State := fun f =>
if f ∈ order then some f else none
/--
The set-based description above really is produced by the operational `step`:
if `p` has not picked yet, their left fork is free and their pickup extends the
recorded history by one entry.
-/
theorem fresh_left_pick_is_a_legal_step
(picked : List Philosopher) (p : Philosopher) (fresh : p ∉ picked) :
step (afterLeftPickups picked) ⟨p, .left⟩ =
some (afterLeftPickups (p :: picked)) := by
have enabled : CanTakeLeft (afterLeftPickups picked) p := by
simp [CanTakeLeft, ForkFree, leftFork, afterLeftPickups, fresh]
simp only [step, enabled, ↓reduceIte]
congr 1
funext f
by_cases same : f = p
· subst f
simp [assign, afterLeftPickups, leftFork, fresh]
· simp [assign, afterLeftPickups, leftFork, same]
theorem every_philosopher_is_listed : ∀ p : Philosopher, p ∈ allPhilosophers := by
native_decide
theorem complete_order_has_no_duplicates
(order : List Philosopher) (h : IsCompletePickupOrder order) :
order.Nodup := by
exact h.nodup_iff.mpr (by native_decide)
theorem complete_order_reaches_allLeft
(order : List Philosopher) (h : IsCompletePickupOrder order) :
afterLeftPickups order = allLeft := by
funext f
have inCanonical : f ∈ allPhilosophers := every_philosopher_is_listed f
have inOrder : f ∈ order := h.mem_iff.mpr inCanonical
simp [afterLeftPickups, allLeft, inOrder]
/--
This is the promised counterexample to the proposed progress argument, in its
strongest order-independent form: *every* permutation of the five left-fork
pickups reaches the same deadlock.
-/
theorem every_left_pick_permutation_deadlocks
(order : List Philosopher) (h : IsCompletePickupOrder order) :
Deadlocked (afterLeftPickups order) := by
rw [complete_order_reaches_allLeft order h]
exact allLeft_is_deadlocked
/-!
## Deadlock freedom versus starvation freedom
Deadlock freedom is a property of reachable states. Starvation freedom is a
strictly more temporal statement: it talks about what must eventually happen
along executions, so it also needs an explicit scheduler-fairness assumption.
-/
/-- States obtainable from `initial` by zero or more successful pickups. -/
inductive Reachable : State → Prop where
| initial : Reachable initial
| next {s t : State} (a : Pickup) :
Reachable s → step s a = some t → Reachable t
/-- Every duplicate-free collection of completed left pickups is reachable. -/
theorem afterLeftPickups_reachable
(picked : List Philosopher) (noDuplicates : picked.Nodup) :
Reachable (afterLeftPickups picked) := by
induction picked with
| nil =>
have emptyPickupsAreInitial :
afterLeftPickups [] = DiningPhilosophers.initial := by
funext f
simp [afterLeftPickups, DiningPhilosophers.initial]
rw [emptyPickupsAreInitial]
exact Reachable.initial
| cons p ps inductionHypothesis =>
have parts := List.nodup_cons.mp noDuplicates
have tailReachable := inductionHypothesis parts.2
exact Reachable.next ⟨p, .left⟩ tailReachable
(fresh_left_pick_is_a_legal_step ps p parts.1)
theorem allLeft_reachable : Reachable allLeft := by
have reached := afterLeftPickups_reachable allPhilosophers (by native_decide)
have complete : IsCompletePickupOrder allPhilosophers := List.Perm.refl _
rw [complete_order_reaches_allLeft allPhilosophers complete] at reached
exact reached
/-- The naive protocol would be deadlock-free if no reachable state deadlocked. -/
def ProtocolDeadlockFree : Prop :=
∀ s, Reachable s → ¬ Deadlocked s
/-- The reachable `allLeft` state refutes deadlock freedom. -/
theorem naive_protocol_is_not_deadlock_free : ¬ ProtocolDeadlockFree := by
intro claimedDeadlockFreedom
exact claimedDeadlockFreedom allLeft allLeft_reachable allLeft_is_deadlocked
/--
An infinite execution may either take a successful step or stutter for one
scheduler tick. Stuttering is needed to discuss executions after a deadlock and
executions in which the scheduler delays an enabled philosopher.
-/
abbrev Execution := Nat → State
def ValidExecution (execution : Execution) : Prop :=
∀ n,
execution (n + 1) = execution n ∨
∃ a, step (execution n) a = some (execution (n + 1))
def ActionEnabled (s : State) (a : Pickup) : Prop :=
∃ t, step s a = some t
def ActionTaken (execution : Execution) (n : Nat) (a : Pickup) : Prop :=
step (execution n) a = some (execution (n + 1))
/--
Weak fairness: if one particular pickup remains enabled at every time from `n`
onwards, then that pickup is eventually taken. This rules out a scheduler that
ignores a continuously enabled action forever.
-/
def WeaklyFair (execution : Execution) : Prop :=
∀ a n,
(∀ m, n ≤ m → ActionEnabled (execution m) a) →
∃ m, n ≤ m ∧ ActionTaken execution m a
/--
Holding the left fork without holding the right records a concrete outstanding
request to eat in this minimal model.
-/
def WaitingToEat (s : State) (p : Philosopher) : Prop :=
Holds s p (leftFork p) ∧ ¬ Eating s p
/--
Trace-level starvation freedom: every outstanding request is eventually
satisfied on that execution.
-/
def StarvationFree (execution : Execution) : Prop :=
∀ p n, WaitingToEat (execution n) p →
∃ m, n ≤ m ∧ Eating (execution m) p
/--
Protocol-level starvation freedom quantifies over every weakly fair execution
starting at any reachable state. Quantifying over reachable suffixes is
equivalent to examining suffixes of executions beginning at `initial`.
-/
def ProtocolStarvationFree : Prop :=
∀ s, Reachable s →
∀ execution, execution 0 = s →
ValidExecution execution → WeaklyFair execution →
StarvationFree execution
/-- The infinite execution which remains in the reachable circular deadlock. -/
def frozenDeadlock : Execution := fun _ => allLeft
theorem frozenDeadlock_is_valid : ValidExecution frozenDeadlock := by
intro n
exact Or.inl rfl
/--
The frozen deadlock is weakly fair, vacuously: no pickup is enabled, so no action
is continuously enabled and fairness creates no obligation.
-/
theorem frozenDeadlock_is_weakly_fair : WeaklyFair frozenDeadlock := by
intro a n continuouslyEnabled
have enabledNow := continuouslyEnabled n (Nat.le_refl n)
rcases enabledNow with ⟨t, h⟩
simp [frozenDeadlock, allLeft_step_is_disabled] at h
theorem every_philosopher_waits_in_allLeft (p : Philosopher) :
WaitingToEat allLeft p := by
exact ⟨rfl, nobody_eats_in_allLeft p⟩
theorem frozenDeadlock_is_not_starvation_free :
¬ StarvationFree frozenDeadlock := by
intro claimedStarvationFreedom
have eventuallyEats :=
claimedStarvationFreedom 0 0 (every_philosopher_waits_in_allLeft 0)
rcases eventuallyEats with ⟨m, _, eats⟩
exact nobody_eats_in_allLeft 0 eats
/--
Even after adding weak scheduler fairness, the naive protocol is not
starvation-free: a reachable deadlock gives a fair infinite execution on which
every philosopher waits forever.
-/
theorem naive_protocol_is_not_starvation_free : ¬ ProtocolStarvationFree := by
intro claimedStarvationFreedom
have traceIsStarvationFree :=
claimedStarvationFreedom allLeft allLeft_reachable frozenDeadlock rfl
frozenDeadlock_is_valid frozenDeadlock_is_weakly_fair
exact frozenDeadlock_is_not_starvation_free traceIsStarvationFree
/-!
The literal rule "a philosopher cannot pick up two forks in one transition" is
therefore a safety/atomicity rule, not a progress rule. Progress needs an extra
protocol constraint which breaks circular wait (for example, global fork
ordering, a waiter/arbitrator, or atomic acquisition of both forks), plus a
fairness assumption if the intended theorem is temporal starvation-freedom.
-/
#eval (initial.toArray.map (Option.map Fin.val))
#eval (allLeft.toArray.map (Option.map Fin.val))
end DiningPhilosophers
/-!
# A positive dining-philosophers protocol
The naive left-first protocol in `DiningPhilosophers.lean` is neither
deadlock-free nor starvation-free. This file gives a complete positive model
for five philosophers using a central round-robin waiter.
The protocol contract is deliberately explicit:
1. all five philosophers repeatedly request to eat;
2. the waiter visits them in circular order `0, 1, 2, 3, 4, 0, ...`;
3. for the current philosopher, the waiter executes `request`, `grant`, and
`release` in that order;
4. `grant` atomically assigns both adjacent forks, so no philosopher can hold
one fork while waiting for the other;
5. `release` frees both forks and advances the waiter.
Because scheduling is part of the protocol, starvation freedom has a concrete
bound instead of an unproved fairness assumption: from *any* configuration,
every philosopher eats within the next 15 transitions. For five philosophers
the worst actual delay is 14 transitions; `Fin 15` represents offsets `0..14`.
-/
namespace DiningPhilosophers.RoundRobinWaiter
/-- The local phase of the philosopher currently visited by the waiter. -/
inductive Phase where
| thinking
| hungry
| eating
deriving DecidableEq, Repr
/--
Only the waiter's current philosopher needs a stored phase. Every other
philosopher is waiting (`hungry`) for their next turn.
-/
structure Config where
turn : Philosopher
phase : Phase
deriving DecidableEq, Repr
def initialConfig : Config :=
{ turn := 0, phase := .thinking }
/-- Exhaustive elimination principle specialized to the fixed five philosophers. -/
theorem philosopher_cases (p : Philosopher) :
p = 0 ∨ p = 1 ∨ p = 2 ∨ p = 3 ∨ p = 4 := by
have bounded : p.val < 5 := p.isLt
have values :
p.val = 0 ∨ p.val = 1 ∨ p.val = 2 ∨ p.val = 3 ∨ p.val = 4 := by
omega
rcases values with h | h | h | h | h
· left; exact Fin.ext h
· right; left; exact Fin.ext h
· right; right; left; exact Fin.ext h
· right; right; right; left; exact Fin.ext h
· right; right; right; right; exact Fin.ext h
/-- The waiter moves clockwise after a release. -/
def nextTurn (p : Philosopher) : Philosopher :=
rightFork p
/--
The complete deterministic state transition. The three phases make request,
grant/eat, and release observably distinct.
-/
def next : Config → Config
| ⟨p, .thinking⟩ => ⟨p, .hungry⟩
| ⟨p, .hungry⟩ => ⟨p, .eating⟩
| ⟨p, .eating⟩ => ⟨nextTurn p, .thinking⟩
inductive Action where
| request
| grant
| release
deriving DecidableEq, Repr
def scheduledAction : Config → Action
| ⟨_, .thinking⟩ => .request
| ⟨_, .hungry⟩ => .grant
| ⟨_, .eating⟩ => .release
/-- Exactly the action selected by the waiter may occur. -/
def step (c : Config) (a : Action) : Option Config :=
if a = scheduledAction c then some (next c) else none
theorem scheduled_step_succeeds (c : Config) :
step c (scheduledAction c) = some (next c) := by
simp [step]
/-!
## Fork ownership and safety
-/
/--
Forks are free outside the eating phase. While `turn` is eating, that
philosopher owns exactly their two adjacent forks. This is the atomic grant.
-/
def forkState (c : Config) : State :=
match c.phase with
| .eating => fun f =>
if f = leftFork c.turn ∨ f = rightFork c.turn then some c.turn else none
| .thinking => initial
| .hungry => initial
def LocalPhase (c : Config) (p : Philosopher) : Phase :=
if p = c.turn then c.phase else .hungry
def EatingNow (c : Config) (p : Philosopher) : Prop :=
LocalPhase c p = .eating
def WaitingToEat (c : Config) (p : Philosopher) : Prop :=
LocalPhase c p = .hungry
instance (c : Config) (p : Philosopher) : Decidable (EatingNow c p) := by
unfold EatingNow
infer_instance
instance (s : State) (p : Philosopher) :
Decidable (DiningPhilosophers.Eating s p) := by
unfold DiningPhilosophers.Eating Holds
infer_instance
/-- The phase-level and concrete two-fork definitions of eating coincide. -/
theorem eatingNow_iff_holds_both_forks (c : Config) (p : Philosopher) :
EatingNow c p ↔ DiningPhilosophers.Eating (forkState c) p := by
rcases c with ⟨turn, phase⟩
rcases philosopher_cases turn with rfl | rfl | rfl | rfl | rfl <;>
rcases philosopher_cases p with rfl | rfl | rfl | rfl | rfl <;>
cases phase <;> native_decide
/-- No fork can have two distinct owners in any waiter configuration. -/
theorem fork_mutual_exclusion
(c : Config) (f : Fork) (p q : Philosopher)
(hp : Holds (forkState c) p f) (hq : Holds (forkState c) q f) :
p = q := by
exact fork_has_at_most_one_owner (forkState c) f p q hp hq
/-- At most one philosopher can be eating at a time. -/
theorem at_most_one_eating
(c : Config) (p q : Philosopher)
(hp : EatingNow c p) (hq : EatingNow c q) : p = q := by
unfold EatingNow LocalPhase at hp hq
split at hp
next pIsTurn =>
split at hq
next qIsTurn => exact pIsTurn.trans qIsTurn.symm
next => contradiction
next => contradiction
/-!
## Deadlock freedom
-/
def HasForwardMove (c : Config) : Prop :=
∃ a t, step c a = some t
def Deadlocked (c : Config) : Prop :=
¬ HasForwardMove c
inductive Reachable : Config → Prop where
| initial : Reachable initialConfig
| next {c d : Config} (a : Action) :
Reachable c → step c a = some d → Reachable d
def ProtocolDeadlockFree : Prop :=
∀ c, Reachable c → ¬ Deadlocked c
/-- Stronger than reachable-state deadlock freedom: every `Config` can move. -/
theorem every_config_has_a_forward_move (c : Config) : HasForwardMove c := by
exact ⟨scheduledAction c, next c, scheduled_step_succeeds c⟩
theorem every_config_is_not_deadlocked (c : Config) : ¬ Deadlocked c := by
intro isDeadlocked
exact isDeadlocked (every_config_has_a_forward_move c)
/-- The round-robin waiter protocol is deadlock-free. -/
theorem protocol_is_deadlock_free : ProtocolDeadlockFree := by
intro c _
exact every_config_is_not_deadlocked c
/-!
## Executions and starvation freedom
-/
/-- Run the waiter for exactly `ticks` deterministic transitions. -/
def run (start : Config) : Nat → Config
| 0 => start
| n + 1 => next (run start n)
def executionFrom (start : Config) : Nat → Config :=
run start
theorem run_add (start : Config) (n k : Nat) :
run start (n + k) = run (run start n) k := by
induction k with
| zero => simp [run]
| succ k inductionHypothesis =>
simp only [run]
exact congrArg next inductionHypothesis
/--
An execution is valid when it starts in the stated configuration and follows
the waiter's transition on every scheduler tick.
-/
def ValidExecutionFrom (start : Config) (execution : Nat → Config) : Prop :=
execution 0 = start ∧ ∀ n, execution (n + 1) = next (execution n)
theorem valid_execution_is_run
{start : Config} {execution : Nat → Config}
(valid : ValidExecutionFrom start execution) :
execution = executionFrom start := by
funext n
induction n with
| zero => exact valid.1
| succ n inductionHypothesis =>
rw [valid.2 n, inductionHypothesis]
rfl
/-- Every philosopher is served within offsets `0..14` from every state. -/
theorem bounded_service (c : Config) (p : Philosopher) :
∃ offset : Fin 15, EatingNow (run c offset.val) p := by
rcases c with ⟨turn, phase⟩
rcases philosopher_cases turn with rfl | rfl | rfl | rfl | rfl <;>
rcases philosopher_cases p with rfl | rfl | rfl | rfl | rfl <;>
cases phase <;> native_decide
/-- Trace-level starvation freedom for this explicit request/eat cycle. -/
def StarvationFree (execution : Nat → Config) : Prop :=
∀ p n, WaitingToEat (execution n) p →
∃ m, n ≤ m ∧ EatingNow (execution m) p
/--
Every canonical round-robin execution is starvation-free. The proof converts
the finite `bounded_service` witness into an absolute future time `n + offset`.
-/
theorem executionFrom_is_starvation_free (start : Config) :
StarvationFree (executionFrom start) := by
intro p n _
obtain ⟨offset, served⟩ := bounded_service (executionFrom start n) p
refine ⟨n + offset.val, Nat.le_add_right n offset.val, ?_⟩
rw [show executionFrom start (n + offset.val) =
run (executionFrom start n) offset.val by
exact run_add start n offset.val]
exact served
def ProtocolStarvationFree : Prop :=
∀ start, Reachable start →
∀ execution, ValidExecutionFrom start execution →
StarvationFree execution
/-- The round-robin waiter protocol is starvation-free. -/
theorem protocol_is_starvation_free : ProtocolStarvationFree := by
intro start _ execution valid
rw [valid_execution_is_run valid]
exact executionFrom_is_starvation_free start
/-!
The two positive results have different logical shapes:
* `protocol_is_deadlock_free` needs only the existence of the next scheduled
action in each state;
* `protocol_is_starvation_free` uses the cyclic schedule and the finite bound
that every philosopher reaches `.eating` within 15 transitions.
-/
#eval (List.range 15).map fun tick =>
let c := run initialConfig tick
(tick, c.turn.val, c.phase)
end DiningPhilosophers.RoundRobinWaiter
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment