E08255: two locks of the same type are nested but no consistent acquisition order can be statically proven — under some interleaving this will deadlock; protect the state under ONE lock over an owning record (and if the state is durable or shared across instances, a mutex cannot protect it — that is a transaction concern)
What went wrong
An enter() call on one MutexLock was reached while another MutexLock of the SAME type was still held, and the two locks are not provably the same physical object. They sit on caller- or runtime-determined objects — two different parameters of the same type, the same field on two different instances, or a collection / dictionary element chosen at runtime. Because which object each lock belongs to is decided at call time, no single consistent acquisition order can be proven to hold for every execution. Under some interleaving two threads acquire the pair in opposite orders and deadlock.
Rationale
EK9's concurrency contract guarantees no execution can deadlock on a lock-acquisition cycle. For locks of DIFFERENT types, or distinct declared fields/allocations of the same object, a single global order can be proven and the nesting is allowed (E08252 only fires if a real cycle forms). But when the nested locks are the same type on objects the compiler cannot pin to a fixed order — f(p, q) called somewhere as f(p, q) and elsewhere as f(q, p); from.lock then to.lock; or accounts.get(i).lock then accounts.get(j).lock — the order is a property of the runtime arguments, not the source. EK9 refuses such code rather than detect a cycle after the fact, because the unprovable case is the classic dining-philosophers / bank-transfer deadlock and there is no sound static order to verify against.
This is the strict stance: if a lock-acquisition order cannot be reasoned about in 30 seconds by a reader, the compiler refuses it. There is no ordered-multi-acquire escape valve, no lock-ranking annotation, no --lenient flag — the language does not have them. The escape is to refactor to a shape with a provable order.
How to fix it
- Consolidate the protected state under a SINGLE MutexLock over an owning record. If two same-type resources must be updated atomically together, model them as one record guarded by one lock — there is no second lock, so no order question can arise. This is the canonical fix. The [EXAMPLE] section below shows same-type locks of two parameter objects nested (their order is whatever the caller passes, so it cannot be proven), then one MutexLock of an 'AccountPair' record that owns the whole transfer.
- If the data is DURABLE or SHARED ACROSS INSTANCES (rows in a database, records in another process, entries owned by other services), a mutex cannot protect it at all — in-memory locks do not span persistence or process boundaries. That is a transaction concern: use the database's transaction / optimistic-locking facilities, not a MutexLock.
- If the resources are genuinely independent and never need joint atomicity, acquire them sequentially — release the first lock before taking the second. With no overlap there is no nesting and no order to prove.
Common causes
- Bank-transfer style: from.lock then to.lock, where from/to are the same type and callers can pass them in either order.
- Two instances of the same lock-bearing class nested in one function.
- Dining philosophers: a ring of same-type resources each "left then right".
- Collection / dictionary element locks: accounts.get(i).lock then accounts.get(j).lock with i/j chosen at runtime.
- A getter that returns one of several same-type locks depending on a runtime value, then both results nested.
Worked examples
Compilable examples demonstrating this error and the correct fixes:
- ek9 -q 1300 (How do I use multiple MutexLocks correctly — coarse vs fine-grained?)
- ek9 -q 158 (How do I protect shared data from concurrent access in EK9?)
See also
Examples
Every example is compiled by the EK9 compiler: the first fails exactly as captioned, the others fix it.
Fails with E08255: two locks of the same type are nested but no consistent acquisition order can be statically proven — under some interleaving this will deadlock; protect the state under ONE lock over an owning record (and if the state is durable or shared across instances, a mutex cannot protect it — that is a transaction concern)
#!ek9 defines module error.e08255.bad defines class Account balanceLock as MutexLock of Integer: MutexLock(Integer(0)) getLock() as pure <- rtn as MutexLock of Integer: balanceLock default operator ? defines function transfer() -> from as Account to as Account innerKey <- () is MutexKey of Integer as function stdout <- Stdout() stdout.println(lockedItem) outerKey <- (to, innerKey) is MutexKey of Integer as function require to.getLock().enter(innerKey) require from.getLock().enter(outerKey)
Fixed: Keep both balances under one lock over an owning record
#!ek9 defines module error.e08255.good defines record AccountPair source as Integer: 0 target as Integer: 0 default operator ? defines class Ledger accountLock as MutexLock of AccountPair: MutexLock(AccountPair()) transfer() -> amount as Integer key <- (amount) is MutexKey of AccountPair as function lockedItem.source: lockedItem.source - amount lockedItem.target: lockedItem.target + amount require accountLock.enter(key) default operator ?
Questions and answers
These Q&A entries show E08255 and its fix: