Dining Philosophers
A classic concurrency puzzle illustrating how simple individual actions can lead to complex system-level behavior. Through Answer Set Programming, we explore the interplay between resource management and synchronization.Photo Credit: Imagen3A Feast of Thought
The Dining Philosophers problem illustrates how simple individual actions can lead to complex system-level behavior. n philosophers sit around a table, each with an insatiable appetite for a centrally located, infinite bowl of noodles. Between each pair of philosophers lies a single chopstick; to eat, a philosopher must hold both adjacent chopsticks. Philosophers alternate between thinking and eating. They can only pick up one chopstick at a time, and once a chopstick is grasped, they hold onto it until they acquire the second. After eating, both chopsticks are returned to the table.We will use Answer Set Programming (ASP) to model the Dining Philosophers problem, not as a problem to be solved, but as a system to be explored. By formalizing the rules of interaction, we gain insight into emergent behaviors and uncover hidden dynamics within this seemingly simple scenario. This approach allows us to analyze different scenarios and discover the conditions under which desirable or undesirable outcomes arise, such as deadlock or starvation.
Setting the Table
Let's define the essential components — the ingredients — of our model. We'll start with a fixed number of philosophers. This number, represented as a constant in ASP, allows us to easily adjust the size of our problem:#const philosopher_count = 3 .
ASP: Constants in ASP provide a way to parameterize our model. While we'll typically set these explicitly for testing, declaring them up front makes the model's dependencies clear. Think of them like configuration parameters in a traditional program.
Seating Arrangements
Since there's a chopstick between each philosopher, we have an equal number of chopsticks as philosophers:#const chopstick_count = philosopher_count .
philosopher(1..philosopher_count) .
chopstick(1..chopstick_count) .
In order for a philosopher to eat, there must be at least two chopsticks:⊥ ← chopstick_count < 2 .
Unit Test
It cannot be that there is only one philosopher (philosopher_count = 1):
There can be two philosophers (philosopher_count = 2):
The Circular Feast
The spatial arrangement matters. The philosophers are seated around a circular table, each with a designated spot. Chopsticks are placed between each pair, giving every philosopher access to exactly two — one on their left and one on their right. This circularity introduces a "wrap-around" effect where the last philosopher's right chopstick is also the first philosopher's left. We capture this relationship using the location/3 predicate:location(P, C, left) ← philosopher(P), C = P .
location(P, C, right) ← philosopher(P), C = (P \ philosopher_count) + 1 .
{| location/3 |
|---|
| 1 | 1 | left |
| 1 | 2 | right |
| 2 | 2 | left |
| 2 | 3 | right |
| 3 | 1 | right |
| 3 | 3 | left |
} This spatial layout is crucial because it defines which resources each philosopher can access, setting the stage for the conflicts we'll explore later when we introduce when we introduce actions and the dynamics those actions create.ASP: The location/3 predicate creates a relationship between philosophers and chopsticks. Each chopstick appears in two locations — to the right of one philosopher and to the left of another. The modulo operation ("\") handles the wrap-around at the end of the table.
Time for Dinner
Now we get to the heart of the matter: how does the state of these chopsticks change over time? We need to track the state of each chopstick — is it available or is some philosopher clutching it? This state changes over discrete moments in time, like frames in a movie. To model this, we'll use the concept of fluents in ASP. A fluent simply links a property (like "available") to a specific point in time.First, let's define our time horizon — how many moments we want to consider: #const horizon = 1 .
Like philosopher_count, we'll set this explicitly for each test scenario.To make working with time easier, we'll introduce some helper predicates:next(I) ← I = 0..horizon-1 .
always(I) ← I = 0..horizon .
The distinction between next and always is crucial: next(I) only includes instants that have a successor (stopping at horizon-1), while always(I) includes every instant including the final one. This difference becomes essential when we need to distinguish between actions that can occur (which need a "next" state) versus conditions that must hold at every moment.
The Dance of Chopsticks
A chopstick can be either available or held by a specific philosopher. We represent this using the fluent chopstickΔ(Chopstick, Status, Instant):chopstickΔ(C, available, 0) ← chopstick(C) .
{| i | chopstickΔ/3 |
|---|
| 0 | 1 | available | 2 | available |
} This initial rule states that all chopsticks start out available at instant 0.ASP: CharmIQ uses the Δ in the predicate name to represent a temporal/state relationship that should be displayed in a tabular format. The reason we chose this convention is that the delta symbol inherently suggests "change over time" to most readers, making it a natural marker for temporal predicates that deserve special visualization treatment.Looking at the output above, you can see chopstickΔ/3 rendered as a table with:- Time instants (
i) on the left; - The remaining arguments formatted as columns.
This tabular view makes it much easier to understand how states evolve compared to the raw answer set format. Without it, we'd just see a flat list of predicates that would require mental effort to organize temporally. The table immediately shows what's true at each instant.This visualization approach is particularly valuable for modeling state machines and temporal logic where you want to track how properties change over discrete time steps. It transforms what would be an abstract collection of facts into an intuitive timeline view.
The Immutable Laws of the Table
- Inertia: A chopstick's state persists unless explicitly changed. Things on the table tend to stay put!
{ chopstickΔ(C, S, I+1) } ←
chopstickΔ(C, S, I), next(I) .
- Uniqueness: A chopstick can only be in one state at any given time. It can't be both available and held:
⊥ ← #count{ S : chopstickΔ(C, S, I) } != 1,
C=1..chopstick_count, always(I) .
{| i | chopstickΔ/3 |
|---|
| 0 | 1 | available | 2 | available | 3 | available |
| 1 | 1 | available | 2 | available | 3 | available |
| 2 | 1 | available | 2 | available | 3 | available |
| 3 | 1 | available | 2 | available | 3 | available |
} These rules ensure a consistent and predictable evolution of our dining scene.ASP: The inertia rule uses a choice rule ({…}) to say "the state may continue unchanged". This lets other rules (like picking up a chopstick) override the default behavior. The uniqueness constraint uses #count to ensure each chopstick has exactly one state — it can't be both available and held, nor can it lack a state entirely.
The Choreography of Chopsticks
Take: takeΩ/3
When philosopher takes a chopstick at instant i then they are holding that chopstick at the next instant (i + 1):chopstickΔ(C, held(P), I+1) ←
takeΩ(P, C, I),
next(I) .
Notice that when modeling the chopstickΔ state for held chopsticks, we need to track which philosopher is holding it using held(P). This becomes important when we later introduce the release action. We'll need to verify that only the philosopher holding a chopstick can release it.A philosopher may not take a chopstick that is currently being held (regardless of who holds it):⊥ ← takeΩ(P, C, I),
chopstickΔ(C, held(_), I),
always(I) .
A philosopher may not take a chopstick that is not immediately to their left or right:⊥ ← takeΩ(P, C, I),
not location(P, C, _),
always(I) .
A philosopher may only take one chopstick at any instant:⊥ ← philosopher(P),
#count{ C : takeΩ(P, C, I) } > 1,
always(I) .
A chopstick may only be taken by a single philosopher at any instant:Notice that we're allowing (i.e. we're not explicitly excluding) multiple philosophers from taking a chopstick in the same instant.
Unit Tests
Two different philosophers can take two different chopsticks at two different instants:
takeΩ(1, 2, 0) .
takeΩ(2, 1, 1) .
{| i | chopstickΔ/3 |
|---|
| 0 | 1 | available | 2 | available |
| 1 | 1 | available | 2 | held(1) |
| 2 | 1 | held(2) | 2 | held(1) |
| 3 | 1 | held(2) | 2 | held(1) |
} Two different philosophers can take two different chopsticks at the same time:
takeΩ(1, 2, 0) .
takeΩ(2, 1, 0) .
{| i | chopstickΔ/3 |
|---|
| 0 | 1 | available | 2 | available |
| 1 | 1 | held(2) | 2 | held(1) |
| 2 | 1 | held(2) | 2 | held(1) |
| 3 | 1 | held(2) | 2 | held(1) |
} A philosopher cannot take a chopstick that isn't on their immediate left or right:
The same philosopher cannot take multiple chopsticks in the same instant:
takeΩ(1, 2, 0) .
takeΩ(1, 1, 0) .
The same chopstick cannot be taken by multiple philosophers at different instants:
takeΩ(1, 1, 0) .
takeΩ(2, 1, 0) .
The same chopstick cannot be taken by multiple philosophers in the same instant:
takeΩ(1, 1, 0) .
takeΩ(2, 1, 1) .
ASP: Similar to how we use Δ to visualize state changes, we use Ω in predicate names to represent actions that cause those state changes. The tabular visualization helps show the relationship between actions and their effects:- The left side shows when actions occur (
takeΩ/3), with arrows indicating the instant before and after the action - The right side shows the resulting states (
chopstickΔ/3) at each instant
Together, they tell a clear story of cause and effect — we can see exactly how each take action changes which chopsticks are held by which philosophersThis visualization approach makes it much easier to understand and debug action sequences compared to looking at raw predicates. Without it, we'd have to mentally piece together which actions led to which state changes.
Release: releaseΩ/3
When a philosopher releases a chopstick at instant i then that chopstick becomes available at the next instant (i + 1):chopstickΔ(C, available, I+1) ←
releaseΩ(P, C, I),
next(I) .
A philosopher may only release a chopstick that they're holding:⊥ ← releaseΩ(P, C, I),
not chopstickΔ(C, held(P), I),
always(I) .
Notice that there are no constraints around how many chopsticks a philosopher may release at any instant (specifically because they need to be able release both at once) or how many philosophers may release their chopsticks.
Unit Tests
A philosopher can release a chopstick that they hold:takeΩ(1, 2, 0) .
releaseΩ(1, 2, 1) .
{| i | releaseΩ/3 | takeΩ/3 |
|---|
| 0 → 1 | | 1 | 2 |
| 1 → 2 | 1 | 2 | |
| i | chopstickΔ/3 |
|---|
| 0 | 1 | available | 2 | available |
| 1 | 1 | available | 2 | held(1) |
| 2 | 1 | available | 2 | available |
| 3 | 1 | available | 2 | available |
} A philosopher cannot release a chopstick if they aren't holding it:
A philosopher cannot release a chopstick that isn't on their immediate left or right:
Side Dishes
Think of these next predicates as the extras that make our dining experience complete. They're not the main course, but they add flavor and make things easier to digest. These helper predicates encapsulate complex logic into bite-sized pieces, making our constraints more readable and maintainable. They're like a special vocabulary for our dinner party, translating low-level details (like who's holding which chopstick) into high-level concepts (like who's actually eating, or if everyone's stuck).
Eating: eatingΔ/2
A philosopher is eating if they are holding both the chopstick on their left and right. eatingΔ(P, I) ←
chopstickΔ(L, held(P), I), location(P, L, left),
chopstickΔ(R, held(P), I), location(P, R, right),
always(I) .
-eatingΔ(P, I) ←
not eatingΔ(P, I), philosopher(P),
always(I) .
ASP: Notice the use of classical negation (prefixed with -) versus default negation (not) in these convenience predicates. This distinction is crucial:- Classical negation (
-eatingΔ(P,I)) definitively states "philosopher P is not eating at instant I" - Default negation (
not eatingΔ(P,I)) means "we cannot prove philosopher P is eating at instant I"
Classical negation (also called "strong" negation) is particularly important in ASP as it allows us to explicitly represent negative information, unlike Prolog which only has default negation. The pattern we use: predicate(X) ← <positive conditions> .
-predicate(X) ← not predicate(X), <domain> .
ensures we have complete information — every philosopher must be either explicitly eating or explicitly not eating at each instant. This completeness is vital for correctly modeling the dining philosophers' problem where we need to reason about global system properties.The term "classical" comes from classical logic where negation is explicit — something is either true or false. This contrasts with default negation which follows the "closed world assumption" where unprovable facts are assumed false.
Release Both: release_bothΩ/2
When a philosopher has finished eating, they release both chopsticks at the same time:release_bothΩ(P, I) ← eatingΔ(P, I) .
releaseΩ(P, L, I) ← release_bothΩ(P, I), location(P, L, left) .
releaseΩ(P, R, I) ← release_bothΩ(P, I), location(P, R, right) .
Unit Tests
A philosopher can sequentially take both chopsticks and explicitly release them (excluding rule (28)):takeΩ(1, 2, 0) .
takeΩ(1, 1, 1) .
release_bothΩ(1, 2) .
{| i | releaseΩ/3 | takeΩ/3 |
|---|
| 0 → 1 | | 1 | 2 |
| 1 → 2 | | 1 | 1 |
| 2 → 3 | 1 | 1 | 1 | 2 | |
| i | chopstickΔ/3 |
|---|
| 0 | 1 | available | 2 | available |
| 1 | 1 | available | 2 | held(1) |
| 2 | 1 | held(1) | 2 | held(1) |
| 3 | 1 | available | 2 | available |
}
A philosopher can sequentially take both chopsticks and implicitly release them (including rule (28)):takeΩ(1, 2, 0) .
takeΩ(1, 1, 1) .
{| i | releaseΩ/3 | takeΩ/3 |
|---|
| 0 → 1 | | 1 | 2 |
| 1 → 2 | | 1 | 1 |
| 2 → 3 | 1 | 1 | 1 | 2 | |
| i | chopstickΔ/3 | eatingΔ/2 |
|---|
| 0 | 1 | available | 2 | available | 1 | 2 |
| 1 | 1 | available | 2 | held(1) | 1 | 2 |
| 2 | 1 | held(1) | 2 | held(1) | 1 | 2 |
| 3 | 1 | available | 2 | available | 1 | 2 |
} A philosopher cannot release both chopsticks if they aren't holding both:takeΩ(1, 1, 1) .
release_bothΩ(1, 2) .
All Held: all_heldΔ/1
If all chopsticks are being held: all_heldΔ(I) ←
#count{ C : chopstickΔ(C, held(_), I) } = chopstick_count,
always(I) .
-all_heldΔ(I) ←
not all_heldΔ(I),
always(I) .
Unit Test
All chopsticks are held:takeΩ(1, 1, 0) .
takeΩ(2, 2, 0) .
takeΩ(3, 3, 0) .
{| i | all_heldΔ/1 | chopstickΔ/3 |
|---|
| 0 | false | 1 | available | 2 | available | 3 | available |
| 1 | true | 1 | held(1) | 2 | held(2) | 3 | held(3) |
}
None Eating: none_eatingΔ/1
If no philosopher is eating: none_eatingΔ(I) ←
#count{ P : -eatingΔ(P, I) } = philosopher_count,
always(I) .
-none_eatingΔ(I) ←
not none_eatingΔ(I),
always(I) .
Unit Test
One philosopher is eating:takeΩ(1, 1, 0) .
takeΩ(1, 2, 1) .
{| i | releaseΩ/3 | takeΩ/3 |
|---|
| 0 → 1 | | 1 | 1 |
| 1 → 2 | | 1 | 2 |
| 2 → 3 | 1 | 1 | 1 | 2 | |
| i | chopstickΔ/3 | none_eatingΔ/1 |
|---|
| 0 | 1 | available | 2 | available | 3 | available | true |
| 1 | 1 | held(1) | 2 | available | 3 | available | true |
| 2 | 1 | held(1) | 2 | held(1) | 3 | available | false |
} No philosophers are eating:takeΩ(1, 1, 0) .
takeΩ(2, 2, 0) .
takeΩ(3, 3, 0) .
{| i | chopstickΔ/3 | none_eatingΔ/1 |
|---|
| 0 | 1 | available | 2 | available | 3 | available | true |
| 1 | 1 | held(1) | 2 | held(2) | 3 | held(3) | true |
}
Palate Cleanser
The Dining Philosophers problem, a classic concurrency puzzle, presents two potential pitfalls: deadlock and starvation. We'll focus on deadlock for now. Having established the rules of our philosophical table, we'll use them not to solve the dining dilemma, but to explore it. Our ASP model, equipped with carefully crafted convenience predicates, will hunt for scenarios where deadlock emerges, revealing the interplay of actions and consequences.
A Recipe for Deadlock
Deadlock occurs when every philosopher clutches a single chopstick, stubbornly waiting for the second, creating a circular dependency. It's a standstill. Formally, deadlock exists when:- All chopsticks are held (no resources available)
- No philosopher is eating (no progress possible)
Our ASP model elegantly captures this:deadlock(I) ← all_heldΔ(I), none_eatingΔ(I) .
The beauty of this definition is its simplicity — we don't need to explicitly check for circular dependencies or analyze complex state transitions. Instead, we leverage our convenience predicates all_heldΔ/1 and none_eatingΔ/1 to capture the essence of deadlock in a single, readable rule.
Tasting for Trouble
Rather than trying to prove deadlock is impossible, we can use ASP's search capabilities to hunt for deadlock scenarios. We do this by stating that it's impossible for deadlock to not occur:⊥ ← not deadlock(_) .
This constraint forces ASP to either:- Find at least one sequence of actions leading to deadlock
- Prove no such sequence exists (making the program unsatisfiable)
Think of it like a chef searching for what makes a soufflé collapse. By specifically trying to make it fail, we'll either discover the exact recipe for disaster (a deadlock sequence) or prove that under our rules, the soufflé simply can't fall flat!This approach of searching for counterexamples is particularly powerful because when ASP finds a deadlock, we get a complete trace of exactly how it happened. This makes it much easier to understand and fix the underlying issues in our dining algorithm.
Deadlock Deconstruction
Having laid the groundwork, defined the rules, and set the stage, we now embark on a multi-course exploration of deadlock. This exploration aims not just to find deadlock, but to understand how ASP can systematically reveal the conditions under which it arises.
Pièce de résistance: The Classic Deadlock
Let's begin with a deliberate construction of the classic deadlock scenario. Each philosopher, with unwavering resolve, grasps the chopstick to their left:takeΩ(P, C, 0) ← location(P, C, left), philosopher(P) .
{| i | all_heldΔ/1 | chopstickΔ/3 | eatingΔ/2 |
|---|
| 0 | false | 1 | available | 2 | available | 3 | available | 1 | 2 | 3 |
| 1 | true | 1 | held(1) | 2 | held(2) | 3 | held(3) | 1 | 2 | 3 |
deadlock(1)} At instant 1, deadlock occurs because all chopsticks are held (all_heldΔ(1) is true) yet no philosopher is eating (eatingΔ(P, 1) is false for all P). This confirms that our model correctly captures this well-known deadlock scenario. But what other deadlock scenarios might exist?
Amuse-bouche: Exploring the Solution Space
To discover all possible deadlocks within a given timeframe, we leverage ASP's powerful search capabilities. We introduce choice rules, allowing any philosopher to pick up any available chopstick at any instant (excluding rule (28) for explicit release): 1 { takeΩ(P, C, I) : philosopher(P), chopstick(C) } ← next(I) .
{ release_bothΩ(P, I) : philosopher(P) } ← next(I) .
A lower-bound on the the cardinality is enforced to ensure that no instant elapses when nothing occurs. For horizon=1:{| i | all_heldΔ/1 | chopstickΔ/3 | eatingΔ/2 |
|---|
| 0 | false | 1 | available | 2 | available | 3 | available | 1 | 2 | 3 |
| 1 | true | 1 | held(3) | 2 | held(1) | 3 | held(2) | 1 | 2 | 3 |
deadlock(1)}{| i | all_heldΔ/1 | chopstickΔ/3 | eatingΔ/2 |
|---|
| 0 | false | 1 | available | 2 | available | 3 | available | 1 | 2 | 3 |
| 1 | true | 1 | held(1) | 2 | held(2) | 3 | held(3) | 1 | 2 | 3 |
deadlock(1)} ASP now explores all possible combinations of actions, revealing two distinct deadlock scenarios: the one we constructed above and the opposite one where all philosophers simultaneously grab their right chopstick. This demonstrates ASP's power to uncover unintended consequences by systematically exploring the solution space.
Appetizer: Refining the Search
We can further refine / simplify our exploration by modifying the rules of engagement. Let's introduce automatic chopstick release (rule (28)):1 { takeΩ(P, C, I) : philosopher(P), chopstick(C) } ← next(I) .
{| i | all_heldΔ/1 | chopstickΔ/3 | eatingΔ/2 |
|---|
| 0 | false | 1 | available | 2 | available | 3 | available | 1 | 2 | 3 |
| 1 | true | 1 | held(3) | 2 | held(1) | 3 | held(2) | 1 | 2 | 3 |
deadlock(1)}{| i | all_heldΔ/1 | chopstickΔ/3 | eatingΔ/2 |
|---|
| 0 | false | 1 | available | 2 | available | 3 | available | 1 | 2 | 3 |
| 1 | true | 1 | held(1) | 2 | held(2) | 3 | held(3) | 1 | 2 | 3 |
deadlock(1)} This constraint alters the search space, but as we see, the two deadlock scenarios persist.
Expanding the Horizon
Our exploration so far has been limited by the horizon constant. Increasing this value expands the search space, allowing ASP to discover deadlocks that might arise over longer sequences of actions. While the classic deadlock scenarios emerge instantly, real systems often exhibit more subtle timing-dependent behaviors that only become apparent over multiple steps.
A Multi-Course Deadlock (horizon = 2)
With a longer time horizon, we can observe deadlocks that emerge from more intricate sequences of actions. Using the same choice rules as before (allowing any philosopher to take any available chopstick), but extending the horizon to horizon = 2, we find the following scenarios:{| i | all_heldΔ/1 | chopstickΔ/3 | eatingΔ/2 |
|---|
| 0 | false | 1 | available | 2 | available | 3 | available | 1 | 2 | 3 |
| 1 | false | 1 | held(3) | 2 | available | 3 | available | 1 | 2 | 3 |
| 2 | true | 1 | held(3) | 2 | held(1) | 3 | held(2) | 1 | 2 | 3 |
deadlock(2)}{| i | all_heldΔ/1 | chopstickΔ/3 | eatingΔ/2 |
|---|
| 0 | false | 1 | available | 2 | available | 3 | available | 1 | 2 | 3 |
| 1 | false | 1 | held(3) | 2 | available | 3 | held(2) | 1 | 2 | 3 |
| 2 | true | 1 | held(3) | 2 | held(1) | 3 | held(2) | 1 | 2 | 3 |
deadlock(2)}{| i | all_heldΔ/1 | chopstickΔ/3 | eatingΔ/2 |
|---|
| 0 | false | 1 | available | 2 | available | 3 | available | 1 | 2 | 3 |
| 1 | false | 1 | held(3) | 2 | held(1) | 3 | available | 1 | 2 | 3 |
| 2 | true | 1 | held(3) | 2 | held(1) | 3 | held(2) | 1 | 2 | 3 |
deadlock(2)}{| i | all_heldΔ/1 | chopstickΔ/3 | eatingΔ/2 |
|---|
| 0 | false | 1 | available | 2 | available | 3 | available | 1 | 2 | 3 |
| 1 | false | 1 | available | 2 | held(1) | 3 | available | 1 | 2 | 3 |
| 2 | true | 1 | held(3) | 2 | held(1) | 3 | held(2) | 1 | 2 | 3 |
deadlock(2)}{| i | all_heldΔ/1 | chopstickΔ/3 | eatingΔ/2 |
|---|
| 0 | false | 1 | available | 2 | available | 3 | available | 1 | 2 | 3 |
| 1 | false | 1 | available | 2 | held(1) | 3 | held(2) | 1 | 2 | 3 |
| 2 | true | 1 | held(3) | 2 | held(1) | 3 | held(2) | 1 | 2 | 3 |
deadlock(2)} This is only the first 5 results. There are many more …
This example demonstrates how increasing the horizon allows ASP to find deadlocks that require more complex action sequences, revealing subtle timing dependencies that might be missed with a shorter timeframe. It highlights the importance of considering longer time horizons when analyzing concurrent systems for potential deadlocks.
A Taste of the Infinite
While we've explored fixed horizons, real-world applications often need to search unbounded timelines. ASP's incremental solving capabilities (using #program base/0. and #program step(t). directives) allow us to efficiently explore ever-increasing horizons without the memory overhead of encoding the entire timeline at once. This is particularly valuable when searching for minimal-length deadlock scenarios or proving their absence up to arbitrary bounds.The implementation of incremental solving for this model is left as an exercise for the reader, but the key insight is that our temporal predicates (chopstickΔ/3, takeΩ/3, etc.) naturally decompose into per-timestep chunks that can be incrementally grounded and solved.This approach to "unbounded" exploration complements our fixed-horizon analysis while maintaining tractable solving times. It's especially useful when we don't know a priori how many steps might be needed to reach a deadlock state.
Dessert: Sweet Success
Our journey through the Dining Philosophers problem demonstrates the power of Answer Set Programming for both exploring and solving concurrency challenges. We've:- Modeled complex interactions using clear, declarative rules;
- Used ASP to discover deadlock scenarios automatically;
- Proved our resource hierarchy solution prevents deadlock entirely.
While we focused on deadlock, this framework opens up other interesting questions:- How might we model and prevent starvation?
- What other solutions (such as enforcing an order in which chopsticks are picked up) could we implement?
- How does the solution scale with more philosophers?
The beauty of our ASP approach is that we can explore these questions by adding or modifying just a few rules, letting the solver do the heavy lifting of finding edge cases and proving correctness.