Dining philosophers, model-checked

Three philosophers share three forks round a table. The states and moves below are a bigraphical reactive system written in Lean; every drawing is generated from it. Forks on the table lie in the table; a fork a philosopher has picked up is drawn inside that philosopher. Coloured curves are the links that say which forks each philosopher may use.

Explored state graph

The problem

Each philosopher alternates between thinking and eating, and needs both neighbouring forks to eat. In the naive protocol a hungry philosopher picks up the left fork, then the right fork, eats, and puts both down. Each step is one reaction rule: pickLeft i, pickRight i, putDown i.

Why it deadlocks

The trace shown runs pickLeft0, pickLeft1, pickLeft2. Now every philosopher holds a left fork and waits for the right one, but each right fork is a neighbour's left fork, already held. No rule matches, so the system is stuck: a circular wait. The checker finds this state among the 14 reachable ones.

The repair

Philosopher 2 picks up the right fork first (rules pickRightFirst2, pickLeftSecond2). The forks are now taken in one global order, so the circle cannot close: some philosopher can always finish. Of its 12 reachable states none is stuck. The trace shown is a run in which philosopher 2 eats.

The fair protocol

The repair above avoids deadlock, but it does not show that every philosopher keeps a chance to eat. In the fair protocol everyone still picks up the left fork first. A hungry philosopher whose right fork is held by its neighbour puts the left fork back and returns to thinking (rules dropH i and dropE i; the redex says the right fork lies inside the neighbour). The trace runs into the naive deadlock state, philosopher 2 drops its fork, and then philosophers 1, 0 and 2 eat in turn. From every reachable state each philosopher can still go on to eat. A livelock remains: everyone can keep picking up and dropping forever.

What "fair" means here. Nobody is ever locked out: from every reachable state, each philosopher can still come to eat (proved, for each of the three; theorem fair_verdicts). It does not mean that every run feeds everyone: a run in which nobody ever eats exists (proved; the livelock tab). A fair scheduler would exclude that run; that last claim is not proved here.

The state graph and its loop backs

The panel under the player draws every state the checker explored and every move between them, in columns by distance (fewest moves) from the initial state. The unfolding of the reactions is finite: it stops when no move leads to a new state. Even so, many moves lead back to a state already found, at the same or a smaller distance; these loop backs are drawn as dashed curves. They are what makes runs of unbounded length possible in a finite graph: a run that never ends must revisit some state, and if some infinite run avoids eating altogether, then so does a lasso, a finite path out followed by a cycle repeated forever. A state drawn in red has no outgoing move: the system is stuck there. Select a state to show it; a state that is not on the current trace is shown on its own.

What was checked

Property, in the closed-system μ-calculusNaiveRepairedFair
νX. ¬(two neighbours eating) ∧ [→]Xholdsholdsholds
μX. [→]⊥ ∨ ⟨→⟩X (a stuck state is reachable)holdsfailsfails
canEat i = νY. (μX. eating_i ∨ ⟨→⟩X) ∧ [→]Y (philosopher i can always still eat)fails for i = 0holds for i = 0holds for i = 0, 1, 2

How sure we are. Each verdict above is a theorem about the library's bigraphs, checked by the Lean kernel (naive_verdicts, repaired_verdicts). Untrusted code explores the states and writes a certificate (the states, the moves between them, and a renumbering for each move); a checker proved correct (m1Prime) validates it in about 30 seconds and evaluates the formulas on the certified graph. The fair protocol's verdicts are proved the same way (fair_verdicts).