codex/mutual-exclusion-lock-order-state-space.py

mutual-exclusion-lock-order-state-space.py
"""Generate mutual-exclusion-lock-order-state-space.png in the working directory.

Tested with Python 3.14, matplotlib 3.10.7 and numpy 2.3.5.
Exhaust the reachable states of two one-shot lock users under sequential consistency.
An unavailable acquisition blocks; no timeouts, retries or forced unlocks are modeled.
"""

from collections import deque
from pathlib import Path
import json

import matplotlib.pyplot as plt


def successors(state, orders):
    """State is (pc1, pc2, owner_of_A, owner_of_B); owner -1 means free."""
    pcs, owners = list(state[:2]), list(state[2:])
    for thread, pc in enumerate(pcs):
        if pc == 4:
            continue
        first, second = orders[thread]
        lock = [first, second, second, first][pc]
        new_owners, new_pcs = owners.copy(), pcs.copy()
        if pc < 2:
            if owners[lock] != -1:
                continue
            new_owners[lock] = thread
        else:
            assert owners[lock] == thread
            new_owners[lock] = -1
        new_pcs[thread] += 1
        yield tuple(new_pcs + new_owners), thread


def explore(orders):
    initial = (0, 0, -1, -1)
    seen, queue, edges = {initial}, deque([initial]), []
    while queue:
        state = queue.popleft()
        for target, thread in successors(state, orders):
            edges.append((state, target, thread))
            if target not in seen:
                seen.add(target)
                queue.append(target)
    deadlocks = sorted(state for state in seen if state[:2] != (4, 4) and not list(successors(state, orders)))
    # For these programs the progress pair uniquely determines lock ownership.
    assert len({state[:2] for state in seen}) == len(seen)
    return seen, edges, deadlocks


def draw_graph(ax, result, title, orders_text):
    states, edges, deadlocks = result
    for source, target, thread in edges:
        ax.annotate("", xy=target[:2], xytext=source[:2],
                    arrowprops=dict(arrowstyle="->", color=["#156a9a", "#8b4b9a"][thread],
                                    lw=1.7, shrinkA=10, shrinkB=10), zorder=1)
    for state in sorted(states):
        color = "#b83d36" if state in deadlocks else "#32834c" if state[:2] == (4, 4) else "#ffffff"
        ax.scatter(*state[:2], s=250, facecolor=color, edgecolor="#52616b", lw=1.3, zorder=3)
    if deadlocks:
        ax.annotate("both hold their first lock;\nneither can acquire the second", xy=(1, 1), xytext=(2.25, 1.25),
                    fontsize=10, color="#a42b29", ha="center",
                    arrowprops=dict(arrowstyle="->", color="#a42b29"))
    ax.text(4, 4.35, "both finished", color="#28703b", ha="center", fontsize=10)
    ax.set_title(title + "\n" + orders_text, fontsize=12, pad=16)
    ax.set_xlim(-0.4, 4.5)
    ax.set_ylim(-0.4, 4.65)
    ax.set_xticks(range(5))
    ax.set_yticks(range(5))
    ax.set_xlabel("T1 completed lock operations", fontsize=11)
    ax.set_ylabel("T2 completed lock operations", fontsize=11)
    ax.set_aspect("equal")
    ax.spines[["top", "right"]].set_visible(False)
    ax.grid(alpha=0.15)
    ax.text(0.5, -0.22, f"{len(states)} reachable states; {len(deadlocks)} deadlock state(s)",
            transform=ax.transAxes, ha="center", fontsize=10)


def main():
    opposite = explore(((0, 1), (1, 0)))
    ordered = explore(((0, 1), (0, 1)))
    assert opposite[2] == [(1, 1, 0, 1)]
    assert ordered[2] == []
    fig, axes = plt.subplots(1, 2, figsize=(10.8, 6.3), facecolor="white")
    draw_graph(axes[0], opposite, "Opposite acquisition orders", "T1: A, B, release B, release A   |   T2: B, A, release A, release B")
    draw_graph(axes[1], ordered, "One shared acquisition order", "both: A, B, release B, release A")
    fig.text(0.5, 0.085, "Blue arrows: T1 advances. Purple arrows: T2 advances. Only reachable states are drawn.",
             ha="center", fontsize=10)
    fig.text(0.5, 0.045, "Each arrow is one enabled acquire or release. Blocked acquisitions have no outgoing arrow.",
             ha="center", fontsize=10)
    fig.subplots_adjust(left=0.07, right=0.98, top=0.84, bottom=0.26, wspace=0.28)
    path = Path(__file__).stem + ".png"
    fig.savefig(path, dpi=110, facecolor="white", transparent=False)
    plt.close(fig)
    print(json.dumps({"image": path, "opposite": {"states": len(opposite[0]), "edges": len(opposite[1]),
                     "deadlocks": opposite[2]}, "ordered": {"states": len(ordered[0]), "edges": len(ordered[1]),
                     "deadlocks": ordered[2]}}))


if __name__ == "__main__":
    main()