E08252: nested enter() on a different lock identity — potential deadlock from lock-order cycle

Category: E08xxx Pre-IR checks (flow analysis) · Reported in phase: PRE_IR_CHECKS · Classification: NESTED_ENTER_DIFFERENT_LOCK · All error codes

What went wrong

An enter() call on one MutexLock was reached while another MutexLock was still held, and the two locks are different identities — AND the program elsewhere acquires the same pair of locks in the opposite order. The two contradicting acquisition orders form a cycle in the workspace-wide lock-precedence graph: at runtime two threads taking these paths concurrently can deadlock — each holds one lock and waits forever for the other.

Rationale

EK9's concurrency contract guarantees no execution can deadlock on a lock-acquisition cycle. The compiler enforces this by building a program-wide graph of "outer-held → inner-acquired" precedence edges and running Tarjan's SCC algorithm: any strongly-connected component of size ≥ 2 IS a lock-order cycle, and every edge in it gets flagged.

Single-direction nesting of two PROVABLY-ORDERABLE locks (different types, or different field declarations) is safe and not flagged: E08252 only fires when at least two distinct call sites take the same pair in opposite orders — the canonical textbook deadlock. This matches the lockdep (Linux kernel) model brought forward to compile time thanks to EK9's closed-world type system.

NOTE: this "single direction is safe" rule applies only when a global order can be PROVEN. Two locks of the SAME type on caller- or runtime-determined objects (different same-type parameters, or the same field on two different objects) are NOT orderable, so even a single static direction is rejected — that is E08255 (UNPROVABLE_LOCK_ORDER), not E08252.

How to fix it

  1. If both resources genuinely need atomic protection together, consolidate them under a SINGLE MutexLock of a record type. The MutexKey body mutates every field atomically; no second lock exists, so no cycle can form. This is the canonical fix. The [EXAMPLE] section below shows two functions taking the same two locks in opposite orders, then one MutexLock of an 'AccountPair' record whose MutexKey moves an amount between both fields.
  2. If the locks protect orthogonal concerns and don't need joint atomicity, acquire them sequentially — release the first before taking the second. No overlap means no cycle.
  3. If the same method needs different locks, consider whether the class structure is wrong: each method should own exactly one concern. Split into multiple methods if necessary.

Common causes

  • Money-transfer style operations holding two account locks in different orders across methods.
  • Iterating one locked collection while updating another, with the outer/inner roles reversed in two different functions.
  • Cache + database pattern where one method locks cache-then-db, the other locks db-then-cache.
  • The opposing pair reached through getters that resolve to distinct lock fields (e.g. box.getA() / box.getB(), or a delegating getter inner.getA()). The getter is resolved to the underlying field, so the cycle is detected exactly as if the fields were named directly.

Worked examples

Compilable examples demonstrating this error and the correct fixes:

  • ek9 -q 1298 (I got E08252 NESTED_ENTER_DIFFERENT_LOCK — what does 'loc...)
  • 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

  • E08255: unprovable lock order — the same-TYPE sibling of this error. E08252 is the PROVABLE cycle (orderable locks, opposite-order sites); E08255 is same-type locks on caller/runtime objects whose order cannot be proven, rejected even single-direction.
  • E08253: same lock acquired across a thread boundary (related — same identity but cross-thread, which doesn't reenter).
  • E08251: shared state mutation outside lock.

Examples

Every example is compiled by the EK9 compiler: the first fails exactly as captioned, the others fix it.

Fails with E08252: nested enter() on a different lock identity — potential deadlock from lock-order cycle

#!ek9
defines module error.e08252.bad

  defines function

    transferAtoB()
      ->
        lockA as MutexLock of Integer
        lockB as MutexLock of String

      innerKeyB <- () is MutexKey of String as function
        stdout <- Stdout()
        stdout.println(lockedItem)

      outerKeyA <- (lockB, innerKeyB) is MutexKey of Integer as function
        require lockB.enter(innerKeyB)

      require lockA.enter(outerKeyA)

    transferBtoA()
      ->
        lockA as MutexLock of Integer
        lockB as MutexLock of String

      innerKeyA <- () is MutexKey of Integer as function
        stdout <- Stdout()
        stdout.println(lockedItem)

      outerKeyB <- (lockA, innerKeyA) is MutexKey of String as function
        require lockA.enter(innerKeyA)

      require lockB.enter(outerKeyB)

Fixed: Protect the state that changes together under ONE lock

#!ek9
defines module error.e08252.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 E08252 and its fix: