E08258: data inside a MutexLock must be purely contained — assigning 'lockedItem' creates an external reference; use ':=:' (copy), ':~:' (merge), or ':^:' (replace) instead

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

What went wrong

Inside a MutexKey callback body, 'lockedItem' was reference-assigned (via ':=', '=', ':', or ':=?') to a target that survives the callback. The assignment creates a second reference to the lock's protected payload — data that should be PURELY CONTAINED within the MutexLock is now reachable from outside. The lock's exclusivity guarantee is silently defeated.

Rationale

A MutexLock's contract is that only one thread accesses the payload at a time, and that access happens only inside enter() / tryEnter() callbacks. That guarantee relies on the lock holding the ONLY reference to the payload. Any other reference — whether a captured variable, a field on a class, or a global — gives outside code a way to read or mutate the payload without going through enter(), silently bypassing the lock.

How to fix it

  1. Copy the payload out with ':=:' instead of a reference assignment - the variable then holds a copy of the value, never the lock's own object (the [EXAMPLE] section below shows the reference assignment, and the copy)
  2. To change the payload, copy, merge or replace INTO 'lockedItem' (':=:', ':~:', ':^:'), which updates the lock's object in place
  3. If you need to expose a derived value (not the whole payload), compute the derived value inside the callback and copy that out - the length of a locked List, say, rather than the List

Common causes

  • Trying to return the locked value from a method by assigning it to the return variable instead of copying it into the variable.
  • Capturing a class field and assigning lockedItem to it — the captured reference outlives the callback.
  • Mistaking ':=' (reference assignment) for ':=:' (copy). The operators look similar but have very different semantics.

[OPERATORS ON 'lockedItem' INSIDE A MutexKey BODY] Rejected (alias-creating):

  • ':=' reference assignment
  • '=' also assignment
  • ':' declaration assignment
  • ':=?' assign-if-unset

Allowed (in-place mutation, no new reference):

  • ':=:' copy
  • ':~:' merge (fills what is unset; never overwrites a set value)
  • ':^:' replace
  • '+=', '-=', '*=', '/=' compound arithmetic
  • Direct method calls on lockedItem
  • Read access (field reads, calls to pure methods)

See also

  • E08254: MutexLock cannot appear in a generic type parameter
  • E08256: MutexLock construction outside field initialiser
  • E08257: MutexLock argument must be a fresh value

Examples

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

Fails with E08258: data inside a MutexLock must be purely contained — assigning 'lockedItem' creates an external reference; use ':=:' (copy), ':~:' (merge), or ':^:' (replace) instead

#!ek9
defines module error.e08258.bad

  defines class

    LeakingHolder
      lockedName as MutexLock of String: MutexLock("initial")

      //After this method returns, 'escaped' holds a reference to the same
      //String object that lives inside lockedName — outside the lock.
      expose()
        <- escaped as String: String()

        accessKey <- (escaped) is MutexKey of String as function
          escaped := lockedItem

        require lockedName.enter(accessKey)

      default operator ?

Fixed: Copy the locked value out with :=:

#!ek9
defines module error.e08258.good

  defines class

    CopyingHolder
      lockedName as MutexLock of String: MutexLock("initial")

      reveal()
        <- copied as String: String()
        accessKey <- (copied) is MutexKey of String as function
          copied :=: lockedItem
        require lockedName.enter(accessKey)

      default operator ?